feat: Add cosmovisor --help (#10229)
* [10126]: Create the help text and a func for checking if help is requested. * [10126]: Check if help is requested and print it if so. * [10126]: If help is requested, and it's possible, also run the binary with the --help flag. * [10126]: Add a Config method for creating a detailed string. * [10126]: Include the config detail string if the config is okay. * [10126]: Create a MultiError error. * [10126]: Get all configuration errors rather than just one at a time. * [10126]: Create unit tests on the MultiError. * [10126]: Remove an extra space from an output string. * [10126]: Add unit tests on more of the args stuff. * [10126]: Export the environment variable name strings. * [10126]: Move the help command stuff into the new cmd area. * [10126]: Move the unit tests on the help stuff that just got moved. * [10126]: Lint fixes. * [10126]: Export the EnvPreupgradeMaxRetries const and handle its error the same way as the others. * [10126]: Update the args test with the new config entry. * [10126]: Add EnvPreupgradeMaxRetries to the help text. * [10126]: Output the full path in the error when the root isn't a directory. * [10126]: Add some newlines that were missing from some help output. * [10126]: Add a link to the documentation to the help text. * [10126]: Don't allow MultiErrors to be in MultiErrors by extracting sub-errors while flattening. * [10126]: Add some missing function comments. * [10126]: Add changelog entry. * [10126]: Fix changelog pull link. * [10126]: Move multierror into its own errors package, then rename it to just multi. * [10126]: Change the Config DetailString to use the environment variables instead of Config field names. Also add the missing PreupgradeMaxRetries entry to it. * [10126]: Remove the environment variables from the help text and just defer to the cosmovisor README. * [10126]: Update the help text as suggested. * [10126]: Update ShouldGiveHelp. Give help if either name or home env vars aren't set. Give help if the first arg is help or any args are -h or --help. This reflects cobra defaults. * [10126]: Pass all args when running a help command rather than just using --help. * [10126]: Undo the changes to process.go. Instead, if an app is configured, just call it with the provided args. * [10126]: Output help if any arg is help. * [10126]: Reorg imports. * [10126]: Change 'Monitored Upgrade Info File' to just 'Monitored File'. The rest of the filename gives the rest of the context. * [10126]: Move the config error handling and output out of DoHelp and put it into the main Run function. That way, it's not being done twice in two different ways, and the setup is always logged. * [10126]: Make checking for a help request case-insensitive (to match what's done in version). * [10126]: Fix unit test that broke when I changed the Monitored File title. * [10126]: Change some unit test env var stuff to use a struct instead of string slices.
This commit is contained in:
parent
a47bd592e9
commit
9cea19d11b
|
@ -43,7 +43,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
|
||||||
+ [\#10128](https://github.com/cosmos/cosmos-sdk/pull/10128) Change default value of `DAEMON_RESTART_AFTER_UPGRADE` to `true`.
|
+ [\#10128](https://github.com/cosmos/cosmos-sdk/pull/10128) Change default value of `DAEMON_RESTART_AFTER_UPGRADE` to `true`.
|
||||||
+ [\#9999](https://github.com/cosmos/cosmos-sdk/pull/10103) Added `version` command that returns the cosmovisor version and the application version.
|
+ [\#9999](https://github.com/cosmos/cosmos-sdk/pull/10103) Added `version` command that returns the cosmovisor version and the application version.
|
||||||
+ [\#9973](https://github.com/cosmos/cosmos-sdk/pull/10056) Added support for pre-upgrade command in Cosmovisor to be called before the binary is upgraded. Added new environmental variable `DAEMON_PREUPGRADE_MAX_RETRIES` that holds the maximum number of times to reattempt pre-upgrade before failing.
|
+ [\#9973](https://github.com/cosmos/cosmos-sdk/pull/10056) Added support for pre-upgrade command in Cosmovisor to be called before the binary is upgraded. Added new environmental variable `DAEMON_PREUPGRADE_MAX_RETRIES` that holds the maximum number of times to reattempt pre-upgrade before failing.
|
||||||
|
+ [\#10126](https://github.com/cosmos/cosmos-sdk/pull/10229) Added `help`.
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
|
||||||
|
|
|
@ -11,17 +11,19 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
cverrors "github.com/cosmos/cosmos-sdk/cosmovisor/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// environment variable names
|
// environment variable names
|
||||||
const (
|
const (
|
||||||
envHome = "DAEMON_HOME"
|
EnvHome = "DAEMON_HOME"
|
||||||
envName = "DAEMON_NAME"
|
EnvName = "DAEMON_NAME"
|
||||||
envDownloadBin = "DAEMON_ALLOW_DOWNLOAD_BINARIES"
|
EnvDownloadBin = "DAEMON_ALLOW_DOWNLOAD_BINARIES"
|
||||||
envRestartUpgrade = "DAEMON_RESTART_AFTER_UPGRADE"
|
EnvRestartUpgrade = "DAEMON_RESTART_AFTER_UPGRADE"
|
||||||
envSkipBackup = "UNSAFE_SKIP_BACKUP"
|
EnvSkipBackup = "UNSAFE_SKIP_BACKUP"
|
||||||
envInterval = "DAEMON_POLL_INTERVAL"
|
EnvInterval = "DAEMON_POLL_INTERVAL"
|
||||||
envPreupgradeMaxRetries = "DAEMON_PREUPGRADE_MAX_RETRIES"
|
EnvPreupgradeMaxRetries = "DAEMON_PREUPGRADE_MAX_RETRIES"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -67,10 +69,15 @@ func (cfg *Config) UpgradeBin(upgradeName string) string {
|
||||||
// UpgradeDir is the directory named upgrade
|
// UpgradeDir is the directory named upgrade
|
||||||
func (cfg *Config) UpgradeDir(upgradeName string) string {
|
func (cfg *Config) UpgradeDir(upgradeName string) string {
|
||||||
safeName := url.PathEscape(upgradeName)
|
safeName := url.PathEscape(upgradeName)
|
||||||
return filepath.Join(cfg.Home, rootName, upgradesDir, safeName)
|
return filepath.Join(cfg.BaseUpgradeDir(), safeName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpgradeInfoFile is the expected upgrade-info filename created by `x/upgrade/keeper`.
|
// BaseUpgradeDir is the directory containing the named upgrade directories.
|
||||||
|
func (cfg *Config) BaseUpgradeDir() string {
|
||||||
|
return filepath.Join(cfg.Root(), upgradesDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpgradeInfoFilePath is the expected upgrade-info filename created by `x/upgrade/keeper`.
|
||||||
func (cfg *Config) UpgradeInfoFilePath() string {
|
func (cfg *Config) UpgradeInfoFilePath() string {
|
||||||
return filepath.Join(cfg.Home, "data", defaultFilename)
|
return filepath.Join(cfg.Home, "data", defaultFilename)
|
||||||
}
|
}
|
||||||
|
@ -118,72 +125,74 @@ func (cfg *Config) CurrentBin() (string, error) {
|
||||||
// GetConfigFromEnv will read the environmental variables into a config
|
// GetConfigFromEnv will read the environmental variables into a config
|
||||||
// and then validate it is reasonable
|
// and then validate it is reasonable
|
||||||
func GetConfigFromEnv() (*Config, error) {
|
func GetConfigFromEnv() (*Config, error) {
|
||||||
|
var errs []error
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
Home: os.Getenv(envHome),
|
Home: os.Getenv(EnvHome),
|
||||||
Name: os.Getenv(envName),
|
Name: os.Getenv(EnvName),
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if cfg.AllowDownloadBinaries, err = booleanOption(envDownloadBin, false); err != nil {
|
if cfg.AllowDownloadBinaries, err = booleanOption(EnvDownloadBin, false); err != nil {
|
||||||
return nil, err
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
if cfg.RestartAfterUpgrade, err = booleanOption(envRestartUpgrade, true); err != nil {
|
if cfg.RestartAfterUpgrade, err = booleanOption(EnvRestartUpgrade, true); err != nil {
|
||||||
return nil, err
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
if cfg.UnsafeSkipBackup, err = booleanOption(envSkipBackup, false); err != nil {
|
if cfg.UnsafeSkipBackup, err = booleanOption(EnvSkipBackup, false); err != nil {
|
||||||
return nil, err
|
errs = append(errs, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
interval := os.Getenv(envInterval)
|
interval := os.Getenv(EnvInterval)
|
||||||
if interval != "" {
|
if interval != "" {
|
||||||
i, err := strconv.ParseUint(interval, 10, 32)
|
switch i, e := strconv.ParseUint(interval, 10, 32); {
|
||||||
if err != nil {
|
case e != nil:
|
||||||
return nil, err
|
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)
|
cfg.PollInterval = time.Millisecond * time.Duration(i)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
cfg.PollInterval = 300 * time.Millisecond
|
cfg.PollInterval = 300 * time.Millisecond
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cfg.validate(); err != nil {
|
envPreupgradeMaxRetriesVal := os.Getenv(EnvPreupgradeMaxRetries)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
envPreupgradeMaxRetriesVal := os.Getenv(envPreupgradeMaxRetries)
|
|
||||||
if cfg.PreupgradeMaxRetries, err = strconv.Atoi(envPreupgradeMaxRetriesVal); err != nil && envPreupgradeMaxRetriesVal != "" {
|
if cfg.PreupgradeMaxRetries, err = strconv.Atoi(envPreupgradeMaxRetriesVal); err != nil && envPreupgradeMaxRetriesVal != "" {
|
||||||
return nil, fmt.Errorf("%s could not be parsed to int: %w", envPreupgradeMaxRetries, err)
|
errs = append(errs, fmt.Errorf("%s could not be parsed to int: %w", EnvPreupgradeMaxRetries, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errs = append(errs, cfg.validate()...)
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return nil, cverrors.FlattenErrors(errs...)
|
||||||
|
}
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate returns an error if this config is invalid.
|
// validate returns an error if this config is invalid.
|
||||||
// it enforces Home/cosmovisor is a valid directory and exists,
|
// it enforces Home/cosmovisor is a valid directory and exists,
|
||||||
// and that Name is set
|
// and that Name is set
|
||||||
func (cfg *Config) validate() error {
|
func (cfg *Config) validate() []error {
|
||||||
|
var errs []error
|
||||||
if cfg.Name == "" {
|
if cfg.Name == "" {
|
||||||
return errors.New(envName + " is not set")
|
errs = append(errs, errors.New(EnvName+" is not set"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Home == "" {
|
switch {
|
||||||
return errors.New(envHome + " is not set")
|
case cfg.Home == "":
|
||||||
|
errs = append(errs, errors.New(EnvHome+" is not set"))
|
||||||
|
case !filepath.IsAbs(cfg.Home):
|
||||||
|
errs = append(errs, errors.New(EnvHome+" must be an absolute path"))
|
||||||
|
default:
|
||||||
|
switch info, err := os.Stat(cfg.Root()); {
|
||||||
|
case err != nil:
|
||||||
|
errs = append(errs, fmt.Errorf("cannot stat home dir: %w", err))
|
||||||
|
case !info.IsDir():
|
||||||
|
errs = append(errs, fmt.Errorf("%s is not a directory", cfg.Root()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !filepath.IsAbs(cfg.Home) {
|
return errs
|
||||||
return errors.New(envHome + " must be an absolute path")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure the root directory exists
|
|
||||||
info, err := os.Stat(cfg.Root())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot stat home dir: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !info.IsDir() {
|
|
||||||
return fmt.Errorf("%s is not a directory", info.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCurrentUpgrade sets the named upgrade to be the current link, returns error if this binary doesn't exist
|
// SetCurrentUpgrade sets the named upgrade to be the current link, returns error if this binary doesn't exist
|
||||||
|
@ -265,3 +274,40 @@ func booleanOption(name string, defaultVal bool) (bool, error) {
|
||||||
}
|
}
|
||||||
return false, fmt.Errorf("env variable %q must have a boolean value (\"true\" or \"false\"), got %q", name, p)
|
return false, fmt.Errorf("env variable %q must have a boolean value (\"true\" or \"false\"), got %q", name, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DetailString returns a multi-line string with details about this config.
|
||||||
|
func (cfg Config) DetailString() string {
|
||||||
|
configEntries := []struct{ name, value string }{
|
||||||
|
{EnvHome, cfg.Home},
|
||||||
|
{EnvName, cfg.Name},
|
||||||
|
{EnvDownloadBin, fmt.Sprintf("%t", cfg.AllowDownloadBinaries)},
|
||||||
|
{EnvRestartUpgrade, fmt.Sprintf("%t", cfg.RestartAfterUpgrade)},
|
||||||
|
{EnvInterval, fmt.Sprintf("%s", cfg.PollInterval)},
|
||||||
|
{EnvSkipBackup, fmt.Sprintf("%t", cfg.UnsafeSkipBackup)},
|
||||||
|
{EnvPreupgradeMaxRetries, fmt.Sprintf("%d", cfg.PreupgradeMaxRetries)},
|
||||||
|
}
|
||||||
|
derivedEntries := []struct{ name, value string }{
|
||||||
|
{"Root Dir", cfg.Root()},
|
||||||
|
{"Upgrade Dir", cfg.BaseUpgradeDir()},
|
||||||
|
{"Genesis Bin", cfg.GenesisBin()},
|
||||||
|
{"Monitored File", cfg.UpgradeInfoFilePath()},
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("Configurable Values:\n")
|
||||||
|
for _, kv := range configEntries {
|
||||||
|
sb.WriteString(fmt.Sprintf(" %s: %s\n", kv.name, kv.value))
|
||||||
|
}
|
||||||
|
sb.WriteString("Derived Values:\n")
|
||||||
|
dnl := 0
|
||||||
|
for _, kv := range derivedEntries {
|
||||||
|
if len(kv.name) > dnl {
|
||||||
|
dnl = len(kv.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dFmt := fmt.Sprintf(" %%%ds: %%s\n", dnl)
|
||||||
|
for _, kv := range derivedEntries {
|
||||||
|
sb.WriteString(fmt.Sprintf(dFmt, kv.name, kv.value))
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
|
@ -5,8 +5,13 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/cosmovisor/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type argsTestSuite struct {
|
type argsTestSuite struct {
|
||||||
|
@ -17,6 +22,94 @@ func TestArgsTestSuite(t *testing.T) {
|
||||||
suite.Run(t, new(argsTestSuite))
|
suite.Run(t, new(argsTestSuite))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cosmovisorEnv are the string values of environment variables used to configure Cosmovisor.
|
||||||
|
type cosmovisorEnv struct {
|
||||||
|
Home string
|
||||||
|
Name string
|
||||||
|
DownloadBin string
|
||||||
|
RestartUpgrade string
|
||||||
|
SkipBackup string
|
||||||
|
Interval string
|
||||||
|
PreupgradeMaxRetries string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToMap creates a map of the cosmovisorEnv where the keys are the env var names.
|
||||||
|
func (c cosmovisorEnv) ToMap() map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
EnvHome: c.Home,
|
||||||
|
EnvName: c.Name,
|
||||||
|
EnvDownloadBin: c.DownloadBin,
|
||||||
|
EnvRestartUpgrade: c.RestartUpgrade,
|
||||||
|
EnvSkipBackup: c.SkipBackup,
|
||||||
|
EnvInterval: c.Interval,
|
||||||
|
EnvPreupgradeMaxRetries: c.PreupgradeMaxRetries,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets the field in this cosmovisorEnv corresponding to the provided envVar to the given envVal.
|
||||||
|
func (c *cosmovisorEnv) Set(envVar, envVal string) {
|
||||||
|
switch envVar {
|
||||||
|
case EnvHome:
|
||||||
|
c.Home = envVal
|
||||||
|
case EnvName:
|
||||||
|
c.Name = envVal
|
||||||
|
case EnvDownloadBin:
|
||||||
|
c.DownloadBin = envVal
|
||||||
|
case EnvRestartUpgrade:
|
||||||
|
c.RestartUpgrade = envVal
|
||||||
|
case EnvSkipBackup:
|
||||||
|
c.SkipBackup = envVal
|
||||||
|
case EnvInterval:
|
||||||
|
c.Interval = envVal
|
||||||
|
case EnvPreupgradeMaxRetries:
|
||||||
|
c.PreupgradeMaxRetries = envVal
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("Unknown environment variable [%s]. Ccannot set field to [%s]. ", envVar, envVal))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearEnv clears environment variables and what they were.
|
||||||
|
// Designed to be used like this:
|
||||||
|
// initialEnv := clearEnv()
|
||||||
|
// defer setEnv(nil, initialEnv)
|
||||||
|
func (s *argsTestSuite) clearEnv() *cosmovisorEnv {
|
||||||
|
s.T().Logf("Clearing environment variables.")
|
||||||
|
rv := cosmovisorEnv{}
|
||||||
|
for envVar := range rv.ToMap() {
|
||||||
|
rv.Set(envVar, os.Getenv(envVar))
|
||||||
|
s.Require().NoError(os.Unsetenv(envVar))
|
||||||
|
}
|
||||||
|
return &rv
|
||||||
|
}
|
||||||
|
|
||||||
|
// setEnv sets environment variables to the values provided.
|
||||||
|
// If t is not nil, and there's a problem, the test will fail immediately.
|
||||||
|
// If t is nil, problems will just be logged using s.T().
|
||||||
|
func (s *argsTestSuite) setEnv(t *testing.T, env *cosmovisorEnv) {
|
||||||
|
if t == nil {
|
||||||
|
s.T().Logf("Restoring environment variables.")
|
||||||
|
}
|
||||||
|
for envVar, envVal := range env.ToMap() {
|
||||||
|
var err error
|
||||||
|
var msg string
|
||||||
|
if len(envVal) != 0 {
|
||||||
|
err = os.Setenv(envVar, envVal)
|
||||||
|
msg = fmt.Sprintf("setting %s to %s", envVar, envVal)
|
||||||
|
} else {
|
||||||
|
err = os.Unsetenv(envVar)
|
||||||
|
msg = fmt.Sprintf("unsetting %s", envVar)
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case t != nil:
|
||||||
|
require.NoError(t, err, msg)
|
||||||
|
case err != nil:
|
||||||
|
s.T().Logf("error %s: %v", msg, err)
|
||||||
|
default:
|
||||||
|
s.T().Logf("done %s", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *argsTestSuite) TestConfigPaths() {
|
func (s *argsTestSuite) TestConfigPaths() {
|
||||||
cases := map[string]struct {
|
cases := map[string]struct {
|
||||||
cfg Config
|
cfg Config
|
||||||
|
@ -92,11 +185,11 @@ func (s *argsTestSuite) TestValidate() {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
err := tc.cfg.validate()
|
errs := tc.cfg.validate()
|
||||||
if tc.valid {
|
if tc.valid {
|
||||||
s.Require().NoError(err)
|
s.Require().Len(errs, 0)
|
||||||
} else {
|
} else {
|
||||||
s.Require().Error(err)
|
s.Require().Greater(len(errs), 0, "number of errors returned")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,7 +200,7 @@ func (s *argsTestSuite) TestEnsureBin() {
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
|
|
||||||
cfg := Config{Home: absPath, Name: "dummyd"}
|
cfg := Config{Home: absPath, Name: "dummyd"}
|
||||||
s.Require().NoError(cfg.validate())
|
s.Require().Len(cfg.validate(), 0, "validation errors")
|
||||||
|
|
||||||
s.Require().NoError(EnsureBinary(cfg.GenesisBin()))
|
s.Require().NoError(EnsureBinary(cfg.GenesisBin()))
|
||||||
|
|
||||||
|
@ -132,17 +225,19 @@ func (s *argsTestSuite) TestEnsureBin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *argsTestSuite) TestBooleanOption() {
|
func (s *argsTestSuite) TestBooleanOption() {
|
||||||
require := s.Require()
|
initialEnv := s.clearEnv()
|
||||||
|
defer s.setEnv(nil, initialEnv)
|
||||||
|
|
||||||
name := "COSMOVISOR_TEST_VAL"
|
name := "COSMOVISOR_TEST_VAL"
|
||||||
|
|
||||||
check := func(def, expected, isErr bool, msg string) {
|
check := func(def, expected, isErr bool, msg string) {
|
||||||
v, err := booleanOption(name, def)
|
v, err := booleanOption(name, def)
|
||||||
if isErr {
|
if isErr {
|
||||||
require.Error(err)
|
s.Require().Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
require.NoError(err)
|
s.Require().NoError(err)
|
||||||
require.Equal(expected, v, msg)
|
s.Require().Equal(expected, v, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
os.Setenv(name, "")
|
os.Setenv(name, "")
|
||||||
|
@ -168,5 +263,237 @@ func (s *argsTestSuite) TestBooleanOption() {
|
||||||
os.Setenv(name, "TRUE")
|
os.Setenv(name, "TRUE")
|
||||||
check(true, true, false, "should handle true value case not sensitive")
|
check(true, true, false, "should handle true value case not sensitive")
|
||||||
check(false, true, false, "should handle true value case not sensitive")
|
check(false, true, false, "should handle true value case not sensitive")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *argsTestSuite) TestDetailString() {
|
||||||
|
home := "/home"
|
||||||
|
name := "test-name"
|
||||||
|
allowDownloadBinaries := true
|
||||||
|
restartAfterUpgrade := true
|
||||||
|
pollInterval := 406 * time.Millisecond
|
||||||
|
unsafeSkipBackup := false
|
||||||
|
preupgradeMaxRetries := 8
|
||||||
|
cfg := &Config{
|
||||||
|
Home: home,
|
||||||
|
Name: name,
|
||||||
|
AllowDownloadBinaries: allowDownloadBinaries,
|
||||||
|
RestartAfterUpgrade: restartAfterUpgrade,
|
||||||
|
PollInterval: pollInterval,
|
||||||
|
UnsafeSkipBackup: unsafeSkipBackup,
|
||||||
|
PreupgradeMaxRetries: preupgradeMaxRetries,
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedPieces := []string{
|
||||||
|
"Configurable Values:",
|
||||||
|
fmt.Sprintf("%s: %s", EnvHome, home),
|
||||||
|
fmt.Sprintf("%s: %s", EnvName, name),
|
||||||
|
fmt.Sprintf("%s: %t", EnvDownloadBin, allowDownloadBinaries),
|
||||||
|
fmt.Sprintf("%s: %t", EnvRestartUpgrade, restartAfterUpgrade),
|
||||||
|
fmt.Sprintf("%s: %s", EnvInterval, pollInterval),
|
||||||
|
fmt.Sprintf("%s: %t", EnvSkipBackup, unsafeSkipBackup),
|
||||||
|
fmt.Sprintf("%s: %d", EnvPreupgradeMaxRetries, preupgradeMaxRetries),
|
||||||
|
"Derived Values:",
|
||||||
|
fmt.Sprintf("Root Dir: %s", home),
|
||||||
|
fmt.Sprintf("Upgrade Dir: %s", home),
|
||||||
|
fmt.Sprintf("Genesis Bin: %s", home),
|
||||||
|
fmt.Sprintf("Monitored File: %s", home),
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := cfg.DetailString()
|
||||||
|
|
||||||
|
for _, piece := range expectedPieces {
|
||||||
|
s.Assert().Contains(actual, piece)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *argsTestSuite) TestGetConfigFromEnv() {
|
||||||
|
initialEnv := s.clearEnv()
|
||||||
|
defer s.setEnv(nil, initialEnv)
|
||||||
|
|
||||||
|
relPath := filepath.Join("testdata", "validate")
|
||||||
|
absPath, perr := filepath.Abs(relPath)
|
||||||
|
s.Require().NoError(perr)
|
||||||
|
|
||||||
|
newConfig := func(home, name string, downloadBin, restartUpgrade, skipBackup bool, interval, preupgradeMaxRetries int) *Config {
|
||||||
|
return &Config{
|
||||||
|
Home: home,
|
||||||
|
Name: name,
|
||||||
|
AllowDownloadBinaries: downloadBin,
|
||||||
|
RestartAfterUpgrade: restartUpgrade,
|
||||||
|
PollInterval: time.Millisecond * time.Duration(interval),
|
||||||
|
UnsafeSkipBackup: skipBackup,
|
||||||
|
PreupgradeMaxRetries: preupgradeMaxRetries,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
envVals cosmovisorEnv
|
||||||
|
expectedCfg *Config
|
||||||
|
expectedErrCount int
|
||||||
|
}{
|
||||||
|
// EnvHome, EnvName, EnvDownloadBin, EnvRestartUpgrade, EnvSkipBackup, EnvInterval, EnvPreupgradeMaxRetries
|
||||||
|
{
|
||||||
|
name: "all bad",
|
||||||
|
envVals: cosmovisorEnv{"", "", "bad", "bad", "bad", "bad", "bad"},
|
||||||
|
expectedCfg: nil,
|
||||||
|
expectedErrCount: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all good",
|
||||||
|
envVals: cosmovisorEnv{absPath, "testname", "true", "false", "true", "303", "1"},
|
||||||
|
expectedCfg: newConfig(absPath, "testname", true, false, true, 303, 1),
|
||||||
|
expectedErrCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nothing set",
|
||||||
|
envVals: cosmovisorEnv{"", "", "", "", "", "", ""},
|
||||||
|
expectedCfg: nil,
|
||||||
|
expectedErrCount: 2,
|
||||||
|
},
|
||||||
|
// Note: Home and Name tests are done in TestValidate
|
||||||
|
{
|
||||||
|
name: "download bin bad",
|
||||||
|
envVals: cosmovisorEnv{absPath, "testname", "bad", "false", "true", "303", "1"},
|
||||||
|
expectedCfg: nil,
|
||||||
|
expectedErrCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "download bin not set",
|
||||||
|
envVals: cosmovisorEnv{absPath, "testname", "", "false", "true", "303", "1"},
|
||||||
|
expectedCfg: newConfig(absPath, "testname", false, false, true, 303, 1),
|
||||||
|
expectedErrCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "download bin true",
|
||||||
|
envVals: cosmovisorEnv{absPath, "testname", "true", "false", "true", "303", "1"},
|
||||||
|
expectedCfg: newConfig(absPath, "testname", true, false, true, 303, 1),
|
||||||
|
expectedErrCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "download bin false",
|
||||||
|
envVals: cosmovisorEnv{absPath, "testname", "false", "false", "true", "303", "1"},
|
||||||
|
expectedCfg: newConfig(absPath, "testname", false, false, true, 303, 1),
|
||||||
|
expectedErrCount: 0,
|
||||||
|
},
|
||||||
|
// EnvHome, EnvName, EnvDownloadBin, EnvRestartUpgrade, EnvSkipBackup, EnvInterval, EnvPreupgradeMaxRetries
|
||||||
|
{
|
||||||
|
name: "restart upgrade bad",
|
||||||
|
envVals: cosmovisorEnv{absPath, "testname", "true", "bad", "true", "303", "1"},
|
||||||
|
expectedCfg: nil,
|
||||||
|
expectedErrCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "restart upgrade not set",
|
||||||
|
envVals: cosmovisorEnv{absPath, "testname", "true", "", "true", "303", "1"},
|
||||||
|
expectedCfg: newConfig(absPath, "testname", true, true, true, 303, 1),
|
||||||
|
expectedErrCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "restart upgrade true",
|
||||||
|
envVals: cosmovisorEnv{absPath, "testname", "true", "true", "true", "303", "1"},
|
||||||
|
expectedCfg: newConfig(absPath, "testname", true, true, true, 303, 1),
|
||||||
|
expectedErrCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "restart upgrade true",
|
||||||
|
envVals: cosmovisorEnv{absPath, "testname", "true", "false", "true", "303", "1"},
|
||||||
|
expectedCfg: newConfig(absPath, "testname", true, false, true, 303, 1),
|
||||||
|
expectedErrCount: 0,
|
||||||
|
},
|
||||||
|
// EnvHome, EnvName, EnvDownloadBin, EnvRestartUpgrade, EnvSkipBackup, EnvInterval, EnvPreupgradeMaxRetries
|
||||||
|
{
|
||||||
|
name: "skip unsafe backups bad",
|
||||||
|
envVals: cosmovisorEnv{absPath, "testname", "true", "false", "bad", "303", "1"},
|
||||||
|
expectedCfg: nil,
|
||||||
|
expectedErrCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "skip unsafe backups not set",
|
||||||
|
envVals: cosmovisorEnv{absPath, "testname", "true", "false", "", "303", "1"},
|
||||||
|
expectedCfg: newConfig(absPath, "testname", true, false, false, 303, 1),
|
||||||
|
expectedErrCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "skip unsafe backups true",
|
||||||
|
envVals: cosmovisorEnv{absPath, "testname", "true", "false", "true", "303", "1"},
|
||||||
|
expectedCfg: newConfig(absPath, "testname", true, false, true, 303, 1),
|
||||||
|
expectedErrCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "skip unsafe backups false",
|
||||||
|
envVals: cosmovisorEnv{absPath, "testname", "true", "false", "false", "303", "1"},
|
||||||
|
expectedCfg: newConfig(absPath, "testname", true, false, false, 303, 1),
|
||||||
|
expectedErrCount: 0,
|
||||||
|
},
|
||||||
|
// EnvHome, EnvName, EnvDownloadBin, EnvRestartUpgrade, EnvSkipBackup, EnvInterval, EnvPreupgradeMaxRetries
|
||||||
|
{
|
||||||
|
name: "poll interval bad",
|
||||||
|
envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "bad", "1"},
|
||||||
|
expectedCfg: nil,
|
||||||
|
expectedErrCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "poll interval 0",
|
||||||
|
envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "0", "1"},
|
||||||
|
expectedCfg: nil,
|
||||||
|
expectedErrCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "poll interval not set",
|
||||||
|
envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "", "1"},
|
||||||
|
expectedCfg: newConfig(absPath, "testname", false, false, false, 300, 1),
|
||||||
|
expectedErrCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "poll interval 987",
|
||||||
|
envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "987", "1"},
|
||||||
|
expectedCfg: newConfig(absPath, "testname", false, false, false, 987, 1),
|
||||||
|
expectedErrCount: 0,
|
||||||
|
},
|
||||||
|
// EnvHome, EnvName, EnvDownloadBin, EnvRestartUpgrade, EnvSkipBackup, EnvInterval, EnvPreupgradeMaxRetries
|
||||||
|
{
|
||||||
|
name: "prepupgrade max retries bad",
|
||||||
|
envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "406", "bad"},
|
||||||
|
expectedCfg: nil,
|
||||||
|
expectedErrCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prepupgrade max retries 0",
|
||||||
|
envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "406", "0"},
|
||||||
|
expectedCfg: newConfig(absPath, "testname", false, false, false, 406, 0),
|
||||||
|
expectedErrCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prepupgrade max retries not set",
|
||||||
|
envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "406", ""},
|
||||||
|
expectedCfg: newConfig(absPath, "testname", false, false, false, 406, 0),
|
||||||
|
expectedErrCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prepupgrade max retries 5",
|
||||||
|
envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "406", "5"},
|
||||||
|
expectedCfg: newConfig(absPath, "testname", false, false, false, 406, 5),
|
||||||
|
expectedErrCount: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
s.T().Run(tc.name, func(t *testing.T) {
|
||||||
|
s.setEnv(t, &tc.envVals)
|
||||||
|
cfg, err := GetConfigFromEnv()
|
||||||
|
if tc.expectedErrCount == 0 {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
} else {
|
||||||
|
if assert.Error(t, err) {
|
||||||
|
errCount := 1
|
||||||
|
if multi, isMulti := err.(*errors.MultiError); isMulti {
|
||||||
|
errCount = multi.Len()
|
||||||
|
}
|
||||||
|
assert.Equal(t, tc.expectedErrCount, errCount, "error count")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.Equal(t, tc.expectedCfg, cfg, "config")
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/cosmovisor"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHelpText creates the help text multi-line string.
|
||||||
|
func GetHelpText() string {
|
||||||
|
return fmt.Sprintf(`Cosmosvisor - A process manager for Cosmos SDK application binaries.
|
||||||
|
|
||||||
|
Cosmovisor is a wrapper for a Cosmos SDK based App (set using the required %s env variable).
|
||||||
|
It starts the App by passing all provided arguments and monitors the %s/data/upgrade-info.json
|
||||||
|
file to perform an update. The upgrade-info.json file is created by the App x/upgrade module
|
||||||
|
when the blockchain height reaches an approved upgrade proposal. The file includes data from
|
||||||
|
the proposal. Cosmovisor interprets that data to perform an update: switch a current binary
|
||||||
|
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
|
||||||
|
`, cosmovisor.EnvName, cosmovisor.EnvHome)
|
||||||
|
}
|
|
@ -0,0 +1,276 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/cosmovisor"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HelpTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelpTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(HelpTestSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
// cosmovisorHelpEnv are some string values of environment variables used to configure Cosmovisor.
|
||||||
|
type cosmovisorHelpEnv struct {
|
||||||
|
Home string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToMap creates a map of the cosmovisorHelpEnv where the keys are the env var names.
|
||||||
|
func (c cosmovisorHelpEnv) ToMap() map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
cosmovisor.EnvHome: c.Home,
|
||||||
|
cosmovisor.EnvName: c.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets the field in this cosmovisorHelpEnv corresponding to the provided envVar to the given envVal.
|
||||||
|
func (c *cosmovisorHelpEnv) Set(envVar, envVal string) {
|
||||||
|
switch envVar {
|
||||||
|
case cosmovisor.EnvHome:
|
||||||
|
c.Home = envVal
|
||||||
|
case cosmovisor.EnvName:
|
||||||
|
c.Name = envVal
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("Unknown environment variable [%s]. Ccannot set field to [%s]. ", envVar, envVal))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearEnv clears environment variables and returns what they were.
|
||||||
|
// Designed to be used like this:
|
||||||
|
// initialEnv := clearEnv()
|
||||||
|
// defer setEnv(nil, initialEnv)
|
||||||
|
func (s *HelpTestSuite) clearEnv() *cosmovisorHelpEnv {
|
||||||
|
s.T().Logf("Clearing environment variables.")
|
||||||
|
rv := cosmovisorHelpEnv{}
|
||||||
|
for envVar := range rv.ToMap() {
|
||||||
|
rv.Set(envVar, os.Getenv(envVar))
|
||||||
|
s.Require().NoError(os.Unsetenv(envVar))
|
||||||
|
}
|
||||||
|
return &rv
|
||||||
|
}
|
||||||
|
|
||||||
|
// setEnv sets environment variables to the values provided.
|
||||||
|
// If t is not nil, and there's a problem, the test will fail immediately.
|
||||||
|
// If t is nil, problems will just be logged using s.T().
|
||||||
|
func (s *HelpTestSuite) setEnv(t *testing.T, env *cosmovisorHelpEnv) {
|
||||||
|
if t == nil {
|
||||||
|
s.T().Logf("Restoring environment variables.")
|
||||||
|
}
|
||||||
|
for envVar, envVal := range env.ToMap() {
|
||||||
|
var err error
|
||||||
|
var msg string
|
||||||
|
if len(envVal) != 0 {
|
||||||
|
err = os.Setenv(envVar, envVal)
|
||||||
|
msg = fmt.Sprintf("setting %s to %s", envVar, envVal)
|
||||||
|
} else {
|
||||||
|
err = os.Unsetenv(envVar)
|
||||||
|
msg = fmt.Sprintf("unsetting %s", envVar)
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case t != nil:
|
||||||
|
require.NoError(t, err, msg)
|
||||||
|
case err != nil:
|
||||||
|
s.T().Logf("error %s: %v", msg, err)
|
||||||
|
default:
|
||||||
|
s.T().Logf("done %s", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HelpTestSuite) TestShouldGiveHelpEnvVars() {
|
||||||
|
initialEnv := s.clearEnv()
|
||||||
|
defer s.setEnv(nil, initialEnv)
|
||||||
|
|
||||||
|
emptyVal := ""
|
||||||
|
homeVal := "/somehome"
|
||||||
|
nameVal := "somename"
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
envHome *string
|
||||||
|
envName *string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "home set name set",
|
||||||
|
envHome: &homeVal,
|
||||||
|
envName: &nameVal,
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "home not set name not set",
|
||||||
|
envHome: nil,
|
||||||
|
envName: nil,
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "home empty name not set",
|
||||||
|
envHome: &emptyVal,
|
||||||
|
envName: nil,
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "home set name not set",
|
||||||
|
envHome: &homeVal,
|
||||||
|
envName: nil,
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "home not set name empty",
|
||||||
|
envHome: nil,
|
||||||
|
envName: &emptyVal,
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "home empty name empty",
|
||||||
|
envHome: &emptyVal,
|
||||||
|
envName: &emptyVal,
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "home set name empty",
|
||||||
|
envHome: &homeVal,
|
||||||
|
envName: &emptyVal,
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "home not set name set",
|
||||||
|
envHome: nil,
|
||||||
|
envName: &nameVal,
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "home empty name set",
|
||||||
|
envHome: &emptyVal,
|
||||||
|
envName: &nameVal,
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
prepEnv := func(t *testing.T, envVar string, envVal *string) {
|
||||||
|
if envVal == nil {
|
||||||
|
require.NoError(t, os.Unsetenv(cosmovisor.EnvHome))
|
||||||
|
} else {
|
||||||
|
require.NoError(t, os.Setenv(envVar, *envVal))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
s.T().Run(tc.name, func(t *testing.T) {
|
||||||
|
prepEnv(t, cosmovisor.EnvHome, tc.envHome)
|
||||||
|
prepEnv(t, cosmovisor.EnvName, tc.envName)
|
||||||
|
actual := ShouldGiveHelp(nil)
|
||||||
|
assert.Equal(t, tc.expected, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s HelpTestSuite) TestShouldGiveHelpArgs() {
|
||||||
|
initialEnv := s.clearEnv()
|
||||||
|
defer s.setEnv(nil, initialEnv)
|
||||||
|
|
||||||
|
s.setEnv(s.T(), &cosmovisorHelpEnv{"/testhome", "testname"})
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil args",
|
||||||
|
args: nil,
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty args",
|
||||||
|
args: []string{},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one arg random",
|
||||||
|
args: []string{"random"},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "five args random",
|
||||||
|
args: []string{"random1", "--random2", "-r", "random4", "-random5"},
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
s.T().Run(tc.name, func(t *testing.T) {
|
||||||
|
actual := ShouldGiveHelp(tc.args)
|
||||||
|
assert.Equal(t, tc.expected, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HelpTestSuite) TestGetHelpText() {
|
||||||
|
expectedPieces := []string{
|
||||||
|
"Cosmosvisor",
|
||||||
|
cosmovisor.EnvName, cosmovisor.EnvHome,
|
||||||
|
"https://github.com/cosmos/cosmos-sdk/tree/master/cosmovisor/README.md",
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := GetHelpText()
|
||||||
|
for _, piece := range expectedPieces {
|
||||||
|
s.Assert().Contains(actual, piece)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,12 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
// RunCosmovisorCommands executes cosmosvisor commands e.g `cosmovisor version`
|
// RunCosmovisorCommands executes cosmosvisor commands e.g `cosmovisor version`
|
||||||
|
// Returned boolean is whether or not execution should continue.
|
||||||
func RunCosmovisorCommands(args []string) {
|
func RunCosmovisorCommands(args []string) {
|
||||||
if isVersionCommand(args) {
|
switch {
|
||||||
|
case ShouldGiveHelp(args):
|
||||||
|
DoHelp()
|
||||||
|
case isVersionCommand(args):
|
||||||
printVersion()
|
printVersion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/cosmos/cosmos-sdk/cosmovisor"
|
"github.com/cosmos/cosmos-sdk/cosmovisor"
|
||||||
"github.com/cosmos/cosmos-sdk/cosmovisor/cmd/cosmovisor/cmd"
|
"github.com/cosmos/cosmos-sdk/cosmovisor/cmd/cosmovisor/cmd"
|
||||||
|
"github.com/cosmos/cosmos-sdk/cosmovisor/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -19,10 +21,20 @@ func main() {
|
||||||
func Run(args []string) error {
|
func Run(args []string) error {
|
||||||
cmd.RunCosmovisorCommands(args)
|
cmd.RunCosmovisorCommands(args)
|
||||||
|
|
||||||
cfg, err := cosmovisor.GetConfigFromEnv()
|
cfg, cerr := cosmovisor.GetConfigFromEnv()
|
||||||
if err != nil {
|
if cerr != nil {
|
||||||
return err
|
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)
|
launcher, err := cosmovisor.NewLauncher(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MultiError is an error combining multiple other errors.
|
||||||
|
// It will never have 0 or 1 errors. It will always have two or more.
|
||||||
|
type MultiError struct {
|
||||||
|
errs []error
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlattenErrors possibly creates a MultiError.
|
||||||
|
// Nil entries are ignored.
|
||||||
|
// If all provided errors are nil (or nothing is provided), nil is returned.
|
||||||
|
// If only one non-nil error is provided, it is returned unchanged.
|
||||||
|
// If two or more non-nil errors are provided, the returned error will be of type *MultiError
|
||||||
|
// and it will contain each non-nil error.
|
||||||
|
func FlattenErrors(errs ...error) error {
|
||||||
|
rv := MultiError{}
|
||||||
|
for _, err := range errs {
|
||||||
|
if err != nil {
|
||||||
|
if merr, isMerr := err.(*MultiError); isMerr {
|
||||||
|
rv.errs = append(rv.errs, merr.errs...)
|
||||||
|
} else {
|
||||||
|
rv.errs = append(rv.errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch rv.Len() {
|
||||||
|
case 0:
|
||||||
|
return nil
|
||||||
|
case 1:
|
||||||
|
return rv.errs[0]
|
||||||
|
}
|
||||||
|
return &rv
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetErrors gets all the errors that make up this MultiError.
|
||||||
|
func (e MultiError) GetErrors() []error {
|
||||||
|
// Return a copy of the errs slice to prevent alteration of the original slice.
|
||||||
|
rv := make([]error, e.Len())
|
||||||
|
copy(rv, e.errs)
|
||||||
|
return rv
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len gets the number of errors in this MultiError.
|
||||||
|
func (e MultiError) Len() int {
|
||||||
|
return len(e.errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error implements the error interface for a MultiError.
|
||||||
|
func (e *MultiError) Error() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(fmt.Sprintf("%d errors: ", len(e.errs)))
|
||||||
|
for i, err := range e.errs {
|
||||||
|
if i != 0 {
|
||||||
|
sb.WriteString(", ")
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("%d: %v", i+1, err))
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements the string interface for a MultiError.
|
||||||
|
func (e MultiError) String() string {
|
||||||
|
return e.Error()
|
||||||
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MultiErrorTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
|
||||||
|
err1 error
|
||||||
|
err2 error
|
||||||
|
err3 error
|
||||||
|
err4 error
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultiErrorTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(MultiErrorTestSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MultiErrorTestSuite) SetupTest() {
|
||||||
|
s.err1 = errors.New("expected error one")
|
||||||
|
s.err2 = errors.New("expected error two")
|
||||||
|
s.err3 = errors.New("expected error three")
|
||||||
|
s.err3 = errors.New("expected error four")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MultiErrorTestSuite) TestFlattenErrors() {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input []error
|
||||||
|
expected error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "none in nil out",
|
||||||
|
input: []error{},
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil in nil out",
|
||||||
|
input: []error{nil},
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nils in nil out",
|
||||||
|
input: []error{nil, nil, nil},
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one in same out",
|
||||||
|
input: []error{s.err1},
|
||||||
|
expected: s.err1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nils and one in that one out",
|
||||||
|
input: []error{nil, s.err2, nil},
|
||||||
|
expected: s.err2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "two in multi out with both",
|
||||||
|
input: []error{s.err1, s.err2},
|
||||||
|
expected: &MultiError{errs: []error{s.err1, s.err2}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "two and nils in multi out with both",
|
||||||
|
input: []error{nil, s.err1, nil, s.err2, nil},
|
||||||
|
expected: &MultiError{errs: []error{s.err1, s.err2}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "lots in multi out",
|
||||||
|
input: []error{s.err1, s.err2, s.err3, s.err2, s.err1},
|
||||||
|
expected: &MultiError{errs: []error{s.err1, s.err2, s.err3, s.err2, s.err1}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multi and non in one multi out with all",
|
||||||
|
input: []error{&MultiError{errs: []error{s.err1, s.err2}}, s.err3},
|
||||||
|
expected: &MultiError{errs: []error{s.err1, s.err2, s.err3}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non and multi in one multi out with all",
|
||||||
|
input: []error{s.err1, &MultiError{errs: []error{s.err2, s.err3}}},
|
||||||
|
expected: &MultiError{errs: []error{s.err1, s.err2, s.err3}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "two multi in one multi out with all",
|
||||||
|
input: []error{&MultiError{errs: []error{s.err1, s.err2}}, &MultiError{errs: []error{s.err3, s.err4}}},
|
||||||
|
expected: &MultiError{errs: []error{s.err1, s.err2, s.err3, s.err4}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
s.T().Run(tc.name, func(t *testing.T) {
|
||||||
|
actual := FlattenErrors(tc.input...)
|
||||||
|
require.Equal(t, tc.expected, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MultiErrorTestSuite) TestGetErrors() {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
multi MultiError
|
||||||
|
expected []error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "two",
|
||||||
|
multi: MultiError{errs: []error{s.err3, s.err1}},
|
||||||
|
expected: []error{s.err3, s.err1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "three",
|
||||||
|
multi: MultiError{errs: []error{s.err3, s.err1, s.err2}},
|
||||||
|
expected: []error{s.err3, s.err1, s.err2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
s.T().Run(tc.name, func(t *testing.T) {
|
||||||
|
// Make sure it's getting what's expected.
|
||||||
|
actual1 := tc.multi.GetErrors()
|
||||||
|
require.NotSame(t, tc.expected, actual1)
|
||||||
|
require.Equal(t, tc.expected, actual1)
|
||||||
|
// Make sure that changing what was given back doesn't alter the original.
|
||||||
|
actual1[0] = errors.New("unexpected error")
|
||||||
|
actual2 := tc.multi.GetErrors()
|
||||||
|
require.NotEqual(t, actual1, actual2)
|
||||||
|
require.Equal(t, tc.expected, actual2)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MultiErrorTestSuite) TestLen() {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
multi MultiError
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "two",
|
||||||
|
multi: MultiError{errs: []error{s.err3, s.err1}},
|
||||||
|
expected: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "three",
|
||||||
|
multi: MultiError{errs: []error{s.err3, s.err1, s.err2}},
|
||||||
|
expected: 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
s.T().Run(tc.name, func(t *testing.T) {
|
||||||
|
actual := tc.multi.Len()
|
||||||
|
require.Equal(t, tc.expected, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MultiErrorTestSuite) TestErrorAndString() {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
multi MultiError
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "two",
|
||||||
|
multi: MultiError{errs: []error{s.err1, s.err2}},
|
||||||
|
expected: fmt.Sprintf("2 errors: 1: %s, 2: %s", s.err1, s.err2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "three",
|
||||||
|
multi: MultiError{errs: []error{s.err1, s.err2, s.err3}},
|
||||||
|
expected: fmt.Sprintf("3 errors: 1: %s, 2: %s, 3: %s", s.err1, s.err2, s.err3),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
s.T().Run(tc.name+" Error", func(t *testing.T) {
|
||||||
|
actual := tc.multi.Error()
|
||||||
|
require.Equal(t, tc.expected, actual)
|
||||||
|
})
|
||||||
|
s.T().Run(tc.name+" String", func(t *testing.T) {
|
||||||
|
actual := tc.multi.String()
|
||||||
|
require.Equal(t, tc.expected, actual)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue