feat(cosmovisor): load cosmovisor configuration from toml file (#19995)

This commit is contained in:
Aryan Tikarya 2024-05-30 19:45:13 +05:30 committed by GitHub
parent 90cbb022d5
commit 465410c75b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 537 additions and 94 deletions

View File

@ -38,6 +38,18 @@ Ref: https://keepachangelog.com/en/1.0.0/
* [#20062](https://github.com/cosmos/cosmos-sdk/pull/20062) Fixed cosmovisor add-upgrade permissions
## Features
* [#19764](https://github.com/cosmos/cosmos-sdk/issues/19764) Use config file for cosmovisor configuration.
## Improvements
* [#19995](https://github.com/cosmos/cosmos-sdk/pull/19995):
* `init command` writes the configuration to the config file only at the default path `DAEMON_HOME/cosmovisor/config.toml`.
* Provide `--cosmovisor-config` flag with value as args to provide the path to the configuration file in the `run` command. `run --cosmovisor-config <path> (other cmds with flags) ...`.
* Add `--cosmovisor-config` flag to provide `config.toml` path to the configuration file in root command used by `add-upgrade` and `config` subcommands.
* `config command` now displays the configuration from the config file if it is provided. If the config file is not provided, it will display the configuration from the environment variables.
## v1.5.0 - 2023-07-17
## Features

View File

@ -12,6 +12,9 @@ import (
"strings"
"time"
"github.com/pelletier/go-toml/v2"
"github.com/spf13/viper"
"cosmossdk.io/log"
"cosmossdk.io/x/upgrade/plan"
upgradetypes "cosmossdk.io/x/upgrade/types"
@ -42,26 +45,29 @@ const (
genesisDir = "genesis"
upgradesDir = "upgrades"
currentLink = "current"
cfgFileName = "config"
cfgExtension = "toml"
)
// Config is the information passed in to control the daemon
type Config struct {
Home string
Name string
AllowDownloadBinaries bool
DownloadMustHaveChecksum bool
RestartAfterUpgrade bool
RestartDelay time.Duration
ShutdownGrace time.Duration
PollInterval time.Duration
UnsafeSkipBackup bool
DataBackupPath string
PreupgradeMaxRetries int
DisableLogs bool
ColorLogs bool
TimeFormatLogs string
CustomPreupgrade string
DisableRecase bool
Home string `toml:"daemon_home" mapstructure:"daemon_home"`
Name string `toml:"daemon_name" mapstructure:"daemon_name"`
AllowDownloadBinaries bool `toml:"daemon_allow_download_binaries" mapstructure:"daemon_allow_download_binaries" default:"false"`
DownloadMustHaveChecksum bool `toml:"daemon_download_must_have_checksum" mapstructure:"daemon_download_must_have_checksum" default:"false"`
RestartAfterUpgrade bool `toml:"daemon_restart_after_upgrade" mapstructure:"daemon_restart_after_upgrade" default:"true"`
RestartDelay time.Duration `toml:"daemon_restart_delay" mapstructure:"daemon_restart_delay"`
ShutdownGrace time.Duration `toml:"daemon_shutdown_grace" mapstructure:"daemon_shutdown_grace"`
PollInterval time.Duration `toml:"daemon_poll_interval" mapstructure:"daemon_poll_interval" default:"300ms"`
UnsafeSkipBackup bool `toml:"unsafe_skip_backup" mapstructure:"unsafe_skip_backup" default:"false"`
DataBackupPath string `toml:"daemon_data_backup_dir" mapstructure:"daemon_data_backup_dir"`
PreUpgradeMaxRetries int `toml:"daemon_preupgrade_max_retries" mapstructure:"daemon_preupgrade_max_retries" default:"0"`
DisableLogs bool `toml:"cosmovisor_disable_logs" mapstructure:"cosmovisor_disable_logs" default:"false"`
ColorLogs bool `toml:"cosmovisor_color_logs" mapstructure:"cosmovisor_color_logs" default:"true"`
TimeFormatLogs string `toml:"cosmovisor_timeformat_logs" mapstructure:"cosmovisor_timeformat_logs" default:"kitchen"`
CustomPreUpgrade string `toml:"cosmovisor_custom_preupgrade" mapstructure:"cosmovisor_custom_preupgrade" default:""`
DisableRecase bool `toml:"cosmovisor_disable_recase" mapstructure:"cosmovisor_disable_recase" default:"false"`
// currently running upgrade
currentUpgrade upgradetypes.Plan
@ -72,6 +78,11 @@ func (cfg *Config) Root() string {
return filepath.Join(cfg.Home, rootName)
}
// DefaultCfgPath returns the default path to the configuration file.
func (cfg *Config) DefaultCfgPath() string {
return filepath.Join(cfg.Root(), cfgFileName+"."+cfgExtension)
}
// GenesisBin is the path to the genesis binary - must be in place to start manager
func (cfg *Config) GenesisBin() string {
return filepath.Join(cfg.Root(), genesisDir, "bin", cfg.Name)
@ -145,6 +156,51 @@ func (cfg *Config) CurrentBin() (string, error) {
return binpath, nil
}
// GetConfigFromFile will read the configuration from the file at the given path.
// If the file path is not provided, it will try to read the configuration from the ENV variables.
// If a file path is provided and ENV variables are set, they will override the values in the file.
func GetConfigFromFile(filePath string) (*Config, error) {
if filePath == "" {
return GetConfigFromEnv()
}
// ensure the file exist
if _, err := os.Stat(filePath); err != nil {
return nil, fmt.Errorf("config not found: at %s : %w", filePath, err)
}
// read the configuration from the file
viper.SetConfigFile(filePath)
// load the env variables
// if the env variable is set, it will override the value provided by the config
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
cfg := &Config{}
if err := viper.Unmarshal(cfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal configuration: %w", err)
}
var (
err error
errs []error
)
if cfg.TimeFormatLogs, err = getTimeFormatOption(cfg.TimeFormatLogs); err != nil {
errs = append(errs, err)
}
errs = append(errs, cfg.validate()...)
if len(errs) > 0 {
return nil, errors.Join(errs...)
}
return cfg, nil
}
// GetConfigFromEnv will read the environmental variables into a config
// and then validate it is reasonable
func GetConfigFromEnv() (*Config, error) {
@ -153,7 +209,7 @@ func GetConfigFromEnv() (*Config, error) {
Home: os.Getenv(EnvHome),
Name: os.Getenv(EnvName),
DataBackupPath: os.Getenv(EnvDataBackupPath),
CustomPreupgrade: os.Getenv(EnvCustomPreupgrade),
CustomPreUpgrade: os.Getenv(EnvCustomPreupgrade),
}
if cfg.DataBackupPath == "" {
@ -220,8 +276,8 @@ func GetConfigFromEnv() (*Config, error) {
}
}
envPreupgradeMaxRetriesVal := os.Getenv(EnvPreupgradeMaxRetries)
if cfg.PreupgradeMaxRetries, err = strconv.Atoi(envPreupgradeMaxRetriesVal); err != nil && envPreupgradeMaxRetriesVal != "" {
envPreUpgradeMaxRetriesVal := os.Getenv(EnvPreupgradeMaxRetries)
if cfg.PreUpgradeMaxRetries, err = strconv.Atoi(envPreUpgradeMaxRetriesVal); err != nil && envPreUpgradeMaxRetriesVal != "" {
errs = append(errs, fmt.Errorf("%s could not be parsed to int: %w", EnvPreupgradeMaxRetries, err))
}
@ -355,6 +411,7 @@ func (cfg *Config) SetCurrentUpgrade(u upgradetypes.Plan) (rerr error) {
return err
}
// UpgradeInfo returns the current upgrade info
func (cfg *Config) UpgradeInfo() (upgradetypes.Plan, error) {
if cfg.currentUpgrade.Name != "" {
return cfg.currentUpgrade, nil
@ -381,7 +438,7 @@ returnError:
return cfg.currentUpgrade, fmt.Errorf("failed to read %q: %w", filename, err)
}
// checks and validates env option
// BooleanOption checks and validate env option
func BooleanOption(name string, defaultVal bool) (bool, error) {
p := strings.ToLower(os.Getenv(name))
switch p {
@ -395,12 +452,17 @@ 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)
}
// checks and validates env option
// TimeFormatOptionFromEnv checks and validates the time format option
func TimeFormatOptionFromEnv(env, defaultVal string) (string, error) {
val, set := os.LookupEnv(env)
if !set {
return defaultVal, nil
}
return getTimeFormatOption(val)
}
func getTimeFormatOption(val string) (string, error) {
switch val {
case "layout":
return time.Layout, nil
@ -432,6 +494,38 @@ func TimeFormatOptionFromEnv(env, defaultVal string) (string, error) {
return "", fmt.Errorf("env variable %q must have a timeformat value (\"layout|ansic|unixdate|rubydate|rfc822|rfc822z|rfc850|rfc1123|rfc1123z|rfc3339|rfc3339nano|kitchen\"), got %q", EnvTimeFormatLogs, val)
}
// ValueToTimeFormatOption converts the time format option to the env value
func ValueToTimeFormatOption(format string) string {
switch format {
case time.Layout:
return "layout"
case time.ANSIC:
return "ansic"
case time.UnixDate:
return "unixdate"
case time.RubyDate:
return "rubydate"
case time.RFC822:
return "rfc822"
case time.RFC822Z:
return "rfc822z"
case time.RFC850:
return "rfc850"
case time.RFC1123:
return "rfc1123"
case time.RFC1123Z:
return "rfc1123z"
case time.RFC3339:
return "rfc3339"
case time.RFC3339Nano:
return "rfc3339nano"
case time.Kitchen:
return "kitchen"
default:
return ""
}
}
// DetailString returns a multi-line string with details about this config.
func (cfg Config) DetailString() string {
configEntries := []struct{ name, value string }{
@ -445,11 +539,11 @@ func (cfg Config) DetailString() string {
{EnvInterval, cfg.PollInterval.String()},
{EnvSkipBackup, fmt.Sprintf("%t", cfg.UnsafeSkipBackup)},
{EnvDataBackupPath, cfg.DataBackupPath},
{EnvPreupgradeMaxRetries, fmt.Sprintf("%d", cfg.PreupgradeMaxRetries)},
{EnvPreupgradeMaxRetries, fmt.Sprintf("%d", cfg.PreUpgradeMaxRetries)},
{EnvDisableLogs, fmt.Sprintf("%t", cfg.DisableLogs)},
{EnvColorLogs, fmt.Sprintf("%t", cfg.ColorLogs)},
{EnvTimeFormatLogs, cfg.TimeFormatLogs},
{EnvCustomPreupgrade, cfg.CustomPreupgrade},
{EnvCustomPreupgrade, cfg.CustomPreUpgrade},
{EnvDisableRecase, fmt.Sprintf("%t", cfg.DisableRecase)},
}
@ -479,3 +573,48 @@ func (cfg Config) DetailString() string {
}
return sb.String()
}
// Export exports the configuration to a file at the given path.
func (cfg Config) Export() (string, error) {
// always use the default path
path := filepath.Clean(cfg.DefaultCfgPath())
// check if config file already exists ask user if they want to overwrite it
if _, err := os.Stat(path); err == nil {
// ask user if they want to overwrite the file
if !askForConfirmation(fmt.Sprintf("file %s already exists, do you want to overwrite it?", path)) {
cfg.Logger(os.Stdout).Info("file already exists, not overriding")
return path, nil
}
}
// create the file
file, err := os.Create(filepath.Clean(path))
if err != nil {
return "", fmt.Errorf("failed to create configuration file: %w", err)
}
// convert the time value to its format option
cfg.TimeFormatLogs = ValueToTimeFormatOption(cfg.TimeFormatLogs)
defer file.Close()
// write the configuration to the file
err = toml.NewEncoder(file).Encode(cfg)
if err != nil {
return "", fmt.Errorf("failed to encode configuration: %w", err)
}
return path, nil
}
func askForConfirmation(str string) bool {
var response string
fmt.Printf("%s [y/n]: ", str)
_, err := fmt.Scanln(&response)
if err != nil {
return false
}
return strings.ToLower(response) == "y"
}

View File

@ -7,6 +7,7 @@ import (
"testing"
"time"
"github.com/pelletier/go-toml/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
@ -412,7 +413,7 @@ func (s *argsTestSuite) TestDetailString() {
PollInterval: pollInterval,
UnsafeSkipBackup: unsafeSkipBackup,
DataBackupPath: dataBackupPath,
PreupgradeMaxRetries: preupgradeMaxRetries,
PreUpgradeMaxRetries: preupgradeMaxRetries,
}
expectedPieces := []string{
@ -444,6 +445,41 @@ func (s *argsTestSuite) TestDetailString() {
}
}
var newConfig = func(
home, name string,
downloadBin bool,
downloadMustHaveChecksum bool,
restartUpgrade bool,
restartDelay int,
skipBackup bool,
dataBackupPath string,
interval, preupgradeMaxRetries int,
disableLogs, colorLogs bool,
timeFormatLogs string,
customPreUpgrade string,
disableRecase bool,
shutdownGrace int,
) *Config {
return &Config{
Home: home,
Name: name,
AllowDownloadBinaries: downloadBin,
DownloadMustHaveChecksum: downloadMustHaveChecksum,
RestartAfterUpgrade: restartUpgrade,
RestartDelay: time.Millisecond * time.Duration(restartDelay),
PollInterval: time.Millisecond * time.Duration(interval),
UnsafeSkipBackup: skipBackup,
DataBackupPath: dataBackupPath,
PreUpgradeMaxRetries: preupgradeMaxRetries,
DisableLogs: disableLogs,
ColorLogs: colorLogs,
TimeFormatLogs: timeFormatLogs,
CustomPreUpgrade: customPreUpgrade,
DisableRecase: disableRecase,
ShutdownGrace: time.Duration(shutdownGrace),
}
}
func (s *argsTestSuite) TestGetConfigFromEnv() {
initialEnv := s.clearEnv()
defer s.setEnv(nil, initialEnv)
@ -452,41 +488,6 @@ func (s *argsTestSuite) TestGetConfigFromEnv() {
absPath, perr := filepath.Abs(relPath)
s.Require().NoError(perr)
newConfig := func(
home, name string,
downloadBin bool,
downloadMustHaveChecksum bool,
restartUpgrade bool,
restartDelay int,
skipBackup bool,
dataBackupPath string,
interval, preupgradeMaxRetries int,
disableLogs, colorLogs bool,
timeFormatLogs string,
customPreUpgrade string,
disableRecase bool,
shutdownGrace int,
) *Config {
return &Config{
Home: home,
Name: name,
AllowDownloadBinaries: downloadBin,
DownloadMustHaveChecksum: downloadMustHaveChecksum,
RestartAfterUpgrade: restartUpgrade,
RestartDelay: time.Millisecond * time.Duration(restartDelay),
PollInterval: time.Millisecond * time.Duration(interval),
UnsafeSkipBackup: skipBackup,
DataBackupPath: dataBackupPath,
PreupgradeMaxRetries: preupgradeMaxRetries,
DisableLogs: disableLogs,
ColorLogs: colorLogs,
TimeFormatLogs: timeFormatLogs,
CustomPreupgrade: customPreUpgrade,
DisableRecase: disableRecase,
ShutdownGrace: time.Duration(shutdownGrace),
}
}
tests := []struct {
name string
envVals cosmovisorEnv
@ -783,6 +784,95 @@ func (s *argsTestSuite) TestGetConfigFromEnv() {
}
}
func (s *argsTestSuite) setUpDir() string {
s.T().Helper()
home := s.T().TempDir()
err := os.MkdirAll(filepath.Join(home, rootName), 0o755)
s.Require().NoError(err)
return home
}
func (s *argsTestSuite) setupConfig(home string) string {
s.T().Helper()
cfg := newConfig(home, "test", true, true, true, 406, false, home, 8, 0, false, true, "kitchen", "", true, 10000000000)
path := filepath.Join(home, rootName, "config.toml")
f, err := os.Create(path)
s.Require().NoError(err)
enc := toml.NewEncoder(f)
s.Require().NoError(enc.Encode(&cfg))
err = f.Close()
s.Require().NoError(err)
return path
}
func (s *argsTestSuite) TestConfigFromFile() {
home := s.setUpDir()
// create a config file
cfgFilePath := s.setupConfig(home)
testCases := []struct {
name string
config *Config
expectedCfg func() *Config
filePath string
expectedError string
malleate func()
}{
{
name: "valid config",
expectedCfg: func() *Config {
return newConfig(home, "test", true, true, true, 406, false, home, 8, 0, false, true, time.Kitchen, "", true, 10000000000)
},
filePath: cfgFilePath,
expectedError: "",
malleate: func() {},
},
{
name: "env variable will override config file fields",
filePath: cfgFilePath,
expectedError: "",
malleate: func() {
// set env variable different from the config file
os.Setenv(EnvName, "env-name")
},
expectedCfg: func() *Config {
return newConfig(home, "env-name", true, true, true, 406, false, home, 8, 0, false, true, time.Kitchen, "", true, 10000000000)
},
},
{
name: "empty config file path will load config from ENV variables",
expectedCfg: func() *Config {
return newConfig(home, "test", true, true, true, 406, false, home, 8, 0, false, true, time.Kitchen, "", true, 10000000000)
},
filePath: "",
expectedError: "",
malleate: func() {
s.setEnv(s.T(), &cosmovisorEnv{home, "test", "true", "true", "true", "406ms", "false", home, "8ms", "0", "false", "true", "kitchen", "", "true", "10s"})
},
},
}
for _, tc := range testCases {
s.T().Run(tc.name, func(t *testing.T) {
tc.malleate()
actualCfg, err := GetConfigFromFile(tc.filePath)
if tc.expectedError != "" {
s.Require().NoError(err)
s.Require().Contains(err.Error(), tc.expectedError)
return
}
s.Require().NoError(err)
s.Require().EqualValues(tc.expectedCfg(), actualCfg)
})
}
}
var sink interface{}
func BenchmarkDetailString(b *testing.B) {
@ -791,7 +881,7 @@ func BenchmarkDetailString(b *testing.B) {
AllowDownloadBinaries: true,
UnsafeSkipBackup: true,
PollInterval: 450 * time.Second,
PreupgradeMaxRetries: 1e7,
PreUpgradeMaxRetries: 1e7,
}
b.ReportAllocs()

View File

@ -30,7 +30,12 @@ func NewAddUpgradeCmd() *cobra.Command {
// AddUpgrade adds upgrade info to manifest
func AddUpgrade(cmd *cobra.Command, args []string) error {
cfg, err := cosmovisor.GetConfigFromEnv()
configPath, err := cmd.Flags().GetString(cosmovisor.FlagCosmovisorConfig)
if err != nil {
return fmt.Errorf("failed to get config flag: %w", err)
}
cfg, err := cosmovisor.GetConfigFromFile(configPath)
if err != nil {
return err
}

View File

@ -7,11 +7,13 @@ import (
)
var configCmd = &cobra.Command{
Use: "config",
Short: "Display cosmovisor config (prints environment variables used by cosmovisor).",
Use: "config",
Short: "Display cosmovisor config.",
Long: `Display cosmovisor config. If a config file is provided, it will display the config from the file,
otherwise it will display the config from the environment variables.`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := cosmovisor.GetConfigFromEnv()
cfg, err := cosmovisor.GetConfigFromFile(cmd.Flag(cosmovisor.FlagCosmovisorConfig).Value.String())
if err != nil {
return err
}

View File

@ -15,14 +15,20 @@ import (
"cosmossdk.io/x/upgrade/plan"
)
var initCmd = &cobra.Command{
Use: "init <path to executable>",
Short: "Initialize a cosmovisor daemon home directory.",
Args: cobra.ExactArgs(1),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
return InitializeCosmovisor(nil, args)
},
func NewIntCmd() *cobra.Command {
initCmd := &cobra.Command{
Use: "init <path to executable>",
Short: "Initialize a cosmovisor daemon home directory.",
Long: `Initialize a cosmovisor daemon home directory with the provided executable.
Configuration file is initialized at the default path (<-home->/cosmovisor/config.toml).`,
Args: cobra.ExactArgs(1),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
return InitializeCosmovisor(nil, args)
},
}
return initCmd
}
// InitializeCosmovisor initializes the cosmovisor directories, current link, and initial executable.
@ -88,12 +94,20 @@ func InitializeCosmovisor(logger log.Logger, args []string) error {
}
logger.Info(fmt.Sprintf("the current symlink points to: %q", cur))
filePath, err := cfg.Export()
if err != nil {
return fmt.Errorf("failed to export configuration: %w", err)
}
logger.Info(fmt.Sprintf("config file present at: %s", filePath))
return nil
}
// getConfigForInitCmd gets just the configuration elements needed to initialize cosmovisor.
func getConfigForInitCmd() (*cosmovisor.Config, error) {
var errs []error
// Note: Not using GetConfigFromEnv here because that checks that the directories already exist.
// We also don't care about the rest of the configuration stuff in here.
cfg := &cosmovisor.Config{
@ -105,19 +119,27 @@ func getConfigForInitCmd() (*cosmovisor.Config, error) {
if cfg.ColorLogs, err = cosmovisor.BooleanOption(cosmovisor.EnvColorLogs, true); err != nil {
errs = append(errs, err)
}
if cfg.TimeFormatLogs, err = cosmovisor.TimeFormatOptionFromEnv(cosmovisor.EnvTimeFormatLogs, time.Kitchen); err != nil {
errs = append(errs, err)
}
// if backup is not set, use the home directory
if cfg.DataBackupPath == "" {
cfg.DataBackupPath = cfg.Home
}
if len(cfg.Name) == 0 {
errs = append(errs, fmt.Errorf("%s is not set", cosmovisor.EnvName))
}
switch {
case len(cfg.Home) == 0:
errs = append(errs, fmt.Errorf("%s is not set", cosmovisor.EnvHome))
case !filepath.IsAbs(cfg.Home):
errs = append(errs, fmt.Errorf("%s must be an absolute path", cosmovisor.EnvHome))
}
if len(errs) > 0 {
return cfg, errors.Join(errs...)
}

View File

@ -9,6 +9,8 @@ import (
"testing"
"time"
"github.com/pelletier/go-toml/v2"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
@ -18,7 +20,10 @@ import (
)
const (
notset = " is not set"
notset = " is not set"
cosmovisorDirName = "cosmovisor"
cfgFileWithExt = "config.toml"
)
type InitTestSuite struct {
@ -79,6 +84,7 @@ func (s *InitTestSuite) clearEnv() *cosmovisorInitEnv {
for envVar := range rv.ToMap() {
rv.Set(envVar, os.Getenv(envVar))
s.Require().NoError(os.Unsetenv(envVar))
viper.Reset()
}
return &rv
}
@ -111,6 +117,26 @@ func (s *InitTestSuite) setEnv(t *testing.T, env *cosmovisorInitEnv) { //nolint:
}
}
// readStdInpFromFile reads the provided data as if it were a standard input.
func (s *InitTestSuite) readStdInpFromFile(data []byte) {
// Create a temporary file and write the test input into it
tmpfile, err := os.CreateTemp("", "test")
if err != nil {
s.T().Fatal(err)
}
// write the test input into the temporary file
if _, err := tmpfile.Write(data); err != nil {
s.T().Fatal(err)
}
if _, err := tmpfile.Seek(0, 0); err != nil {
s.T().Fatal(err)
}
os.Stdin = tmpfile
}
var (
_ io.Reader = BufferedPipe{}
_ io.Writer = BufferedPipe{}
@ -247,7 +273,7 @@ func (p *BufferedPipe) panicIfStarted(msg string) {
func (s *InitTestSuite) NewCapturingLogger() (*BufferedPipe, log.Logger) {
bufferedStdOut, err := StartNewBufferedPipe("stdout", os.Stdout)
s.Require().NoError(err, "creating stdout buffered pipe")
logger := log.NewLogger(bufferedStdOut, log.ColorOption(false), log.TimeFormatOption(time.RFC3339Nano)).With(log.ModuleKey, "cosmovisor")
logger := log.NewLogger(bufferedStdOut, log.ColorOption(false), log.TimeFormatOption(time.RFC3339Nano)).With(log.ModuleKey, cosmovisorDirName)
return &bufferedStdOut, logger
}
@ -360,7 +386,7 @@ func (s *InitTestSuite) TestInitializeCosmovisorInvalidExisting() {
Home: filepath.Join(testDir, "home"),
Name: "pear",
}
genDir := filepath.Join(env.Home, "cosmovisor", "genesis")
genDir := filepath.Join(env.Home, cosmovisorDirName, "genesis")
genBin := filepath.Join(genDir, "bin")
require.NoError(t, os.MkdirAll(genDir, 0o755), "creating genesis directory")
require.NoError(t, copyFile(hwExe, genBin), "copying exe to genesis/bin")
@ -380,7 +406,7 @@ func (s *InitTestSuite) TestInitializeCosmovisorInvalidExisting() {
}
// Create the genesis bin executable path fully as a directory (instead of a file).
// That should get through all the other stuff, but error when EnsureBinary is called.
genBinExe := filepath.Join(env.Home, "cosmovisor", "genesis", "bin", env.Name)
genBinExe := filepath.Join(env.Home, cosmovisorDirName, "genesis", "bin", env.Name)
require.NoError(t, os.MkdirAll(genBinExe, 0o755))
expErr := fmt.Sprintf("%s is not a regular file", env.Name)
// Check the log messages just to make sure it's erroring where expecting.
@ -416,7 +442,7 @@ func (s *InitTestSuite) TestInitializeCosmovisorInvalidExisting() {
Home: filepath.Join(testDir, "home"),
Name: "orange",
}
rootDir := filepath.Join(env.Home, "cosmovisor")
rootDir := filepath.Join(env.Home, cosmovisorDirName)
require.NoError(t, os.MkdirAll(rootDir, 0o755))
curLn := filepath.Join(rootDir, "current")
genDir := filepath.Join(rootDir, "genesis")
@ -465,8 +491,8 @@ func (s *InitTestSuite) TestInitializeCosmovisorValid() {
Home: filepath.Join(testDir, "home"),
Name: "blank",
}
curLn := filepath.Join(env.Home, "cosmovisor", "current")
genBinDir := filepath.Join(env.Home, "cosmovisor", "genesis", "bin")
curLn := filepath.Join(env.Home, cosmovisorDirName, "current")
genBinDir := filepath.Join(env.Home, cosmovisorDirName, "genesis", "bin")
genBinExe := filepath.Join(genBinDir, env.Name)
expInLog := []string{
"checking on the genesis/bin directory",
@ -476,6 +502,7 @@ func (s *InitTestSuite) TestInitializeCosmovisorValid() {
fmt.Sprintf("making sure %q is executable", genBinExe),
"checking on the current symlink and creating it if needed",
fmt.Sprintf("the current symlink points to: %q", genBinExe),
fmt.Sprintf("config file present at: %s", filepath.Join(env.Home, cosmovisorDirName, cfgFileWithExt)),
}
s.setEnv(s.T(), env)
@ -508,7 +535,7 @@ func (s *InitTestSuite) TestInitializeCosmovisorValid() {
Home: filepath.Join(testDir, "home"),
Name: "nocur",
}
rootDir := filepath.Join(env.Home, "cosmovisor")
rootDir := filepath.Join(env.Home, cosmovisorDirName)
genBinDir := filepath.Join(rootDir, "genesis", "bin")
genBinDirExe := filepath.Join(genBinDir, env.Name)
require.NoError(t, os.MkdirAll(genBinDir, 0o755), "making genesis bin dir")
@ -528,6 +555,7 @@ func (s *InitTestSuite) TestInitializeCosmovisorValid() {
fmt.Sprintf("the %q file already exists", genBinDirExe),
fmt.Sprintf("making sure %q is executable", genBinDirExe),
fmt.Sprintf("the current symlink points to: %q", genBinDirExe),
fmt.Sprintf("config file present at: %s", filepath.Join(env.Home, cosmovisorDirName, cfgFileWithExt)),
}
s.setEnv(t, env)
@ -548,7 +576,7 @@ func (s *InitTestSuite) TestInitializeCosmovisorValid() {
Home: filepath.Join(testDir, "home"),
Name: "emptygen",
}
rootDir := filepath.Join(env.Home, "cosmovisor")
rootDir := filepath.Join(env.Home, cosmovisorDirName)
genBinDir := filepath.Join(rootDir, "genesis", "bin")
genBinExe := filepath.Join(genBinDir, env.Name)
require.NoError(t, os.MkdirAll(genBinDir, 0o755), "making genesis bin dir")
@ -560,6 +588,7 @@ func (s *InitTestSuite) TestInitializeCosmovisorValid() {
fmt.Sprintf("copying executable into place: %q", genBinExe),
fmt.Sprintf("making sure %q is executable", genBinExe),
fmt.Sprintf("the current symlink points to: %q", genBinExe),
fmt.Sprintf("config file present at: %s", filepath.Join(env.Home, cosmovisorDirName, cfgFileWithExt)),
}
s.setEnv(t, env)
@ -573,4 +602,111 @@ func (s *InitTestSuite) TestInitializeCosmovisorValid() {
assert.Contains(t, bufferStr, exp)
}
})
s.T().Run("ask to override (y/n) the existing config file", func(t *testing.T) {
})
s.T().Run("init command exports configs to default path", func(t *testing.T) {
testDir := s.T().TempDir()
env := &cosmovisorInitEnv{
Home: filepath.Join(testDir, "home"),
Name: "emptygen",
}
s.setEnv(t, env)
buffer, logger := s.NewCapturingLogger()
logger.Info(fmt.Sprintf("Calling InitializeCosmovisor: %s", t.Name()))
err := InitializeCosmovisor(logger, []string{hwExe})
require.NoError(t, err, "calling InitializeCosmovisor")
bufferBz := buffer.Collect()
bufferStr := string(bufferBz)
assert.Contains(t, bufferStr, fmt.Sprintf("config file present at: %s", filepath.Join(env.Home, cosmovisorDirName, cfgFileWithExt)))
})
}
func (s *InitTestSuite) TestInitializeCosmovisorWithOverrideCfg() {
initEnv := s.clearEnv()
defer s.setEnv(nil, initEnv)
tmpExe := s.CreateHelloWorld(0o755)
testDir := s.T().TempDir()
homePath := filepath.Join(testDir, "backup")
testCases := []struct {
name string
input string
cfg *cosmovisor.Config
override bool
}{
{
name: "yes override",
input: "y\n",
cfg: &cosmovisor.Config{
Home: homePath,
Name: "old_test",
DataBackupPath: homePath,
},
override: true,
},
{
name: "no override",
input: "n\n",
cfg: &cosmovisor.Config{
Home: homePath,
Name: "old_test",
DataBackupPath: homePath,
},
override: false,
},
}
for _, tc := range testCases {
s.T().Run(tc.name, func(t *testing.T) {
// create a root cosmovisor directory
require.NoError(t, os.MkdirAll(tc.cfg.Root(), 0o755), "making root dir")
// create a config file in the default location
file, err := os.Create(tc.cfg.DefaultCfgPath())
require.NoError(t, err)
// write the config to the file
err = toml.NewEncoder(file).Encode(tc.cfg)
require.NoError(t, err)
err = file.Close()
require.NoError(t, err)
s.readStdInpFromFile([]byte(tc.input))
_, logger := s.NewCapturingLogger()
logger.Info(fmt.Sprintf("Calling InitializeCosmovisor: %s", t.Name()))
// override the daemon name in environment file
// if override is true (y), then the name should be updated in the config file
// otherwise (n), the name should not be updated in the config file
s.setEnv(t, &cosmovisorInitEnv{
Home: tc.cfg.Home,
Name: "update_name",
})
err = InitializeCosmovisor(logger, []string{tmpExe})
require.NoError(t, err, "calling InitializeCosmovisor")
cfg := &cosmovisor.Config{}
// read the config file
cfgFile, err := os.Open(tc.cfg.DefaultCfgPath())
require.NoError(t, err)
defer cfgFile.Close()
err = toml.NewDecoder(cfgFile).Decode(cfg)
require.NoError(t, err)
if tc.override {
// check if the name is updated
// basically, override the existing config file
assert.Equal(t, "update_name", cfg.Name)
} else {
// daemon name should not be updated
assert.Equal(t, tc.cfg.Name, cfg.Name)
}
})
}
}

View File

@ -2,6 +2,8 @@ package main
import (
"github.com/spf13/cobra"
"cosmossdk.io/tools/cosmovisor"
)
func NewRootCmd() *cobra.Command {
@ -12,12 +14,13 @@ func NewRootCmd() *cobra.Command {
}
rootCmd.AddCommand(
initCmd,
NewIntCmd(),
runCmd,
configCmd,
NewVersionCmd(),
NewAddUpgradeCmd(),
)
rootCmd.PersistentFlags().StringP(cosmovisor.FlagCosmovisorConfig, "c", "", "path to cosmovisor config file")
return rootCmd
}

View File

@ -1,24 +1,35 @@
package main
import (
"fmt"
"strings"
"github.com/spf13/cobra"
"cosmossdk.io/tools/cosmovisor"
)
var runCmd = &cobra.Command{
Use: "run",
Short: "Run an APP command.",
Use: "run",
Short: "Run an APP command.",
Long: `Run an APP command. This command is intended to be used by the cosmovisor binary.
Provide cosmovisor config file path in command args or set env variables to load configuration.
`,
SilenceUsage: true,
DisableFlagParsing: true,
RunE: func(_ *cobra.Command, args []string) error {
return run(args)
cfgPath, args, err := parseCosmovisorConfig(args)
if err != nil {
return fmt.Errorf("failed to parse cosmovisor config: %w", err)
}
return run(cfgPath, args)
},
}
// run runs the configured program with the given args and monitors it for upgrades.
func run(args []string, options ...RunOption) error {
cfg, err := cosmovisor.GetConfigFromEnv()
func run(cfgPath string, args []string, options ...RunOption) error {
cfg, err := cosmovisor.GetConfigFromFile(cfgPath)
if err != nil {
return err
}
@ -47,3 +58,24 @@ func run(args []string, options ...RunOption) error {
return err
}
func parseCosmovisorConfig(args []string) (string, []string, error) {
var configFilePath string
for i, arg := range args {
// Check if the argument is the config flag
if strings.EqualFold(arg, fmt.Sprintf("--%s", cosmovisor.FlagCosmovisorConfig)) ||
strings.EqualFold(arg, fmt.Sprintf("-%s", cosmovisor.FlagCosmovisorConfig)) {
// Check if there is an argument after the flag which should be the config file path
if i+1 >= len(args) {
return "", nil, fmt.Errorf("--%s requires an argument", cosmovisor.FlagCosmovisorConfig)
}
configFilePath = args[i+1]
// Remove the flag and its value from the arguments
args = append(args[:i], args[i+2:]...)
break
}
}
return configFilePath, args, nil
}

View File

@ -47,7 +47,7 @@ func printVersion(cmd *cobra.Command, args []string, noAppVersion bool) error {
return nil
}
if err := run(append([]string{"version"}, args...)); err != nil {
if err := run("", append([]string{"version"}, args...)); err != nil {
return fmt.Errorf("failed to run version command: %w", err)
}
@ -62,6 +62,7 @@ func printVersionJSON(cmd *cobra.Command, args []string, noAppVersion bool) erro
buf := new(strings.Builder)
if err := run(
"",
[]string{"version", "--long", "--output", "json"},
StdOutRunOption(buf),
); err != nil {

View File

@ -6,4 +6,5 @@ const (
FlagCosmovisorOnly = "cosmovisor-only"
FlagForce = "force"
FlagUpgradeHeight = "upgrade-height"
FlagCosmovisorConfig = "cosmovisor-config"
)

View File

@ -201,7 +201,7 @@ func (l Launcher) doBackup() error {
// doCustomPreUpgrade executes the custom preupgrade script if provided.
func (l Launcher) doCustomPreUpgrade() error {
if l.cfg.CustomPreupgrade == "" {
if l.cfg.CustomPreUpgrade == "" {
return nil
}
@ -221,7 +221,7 @@ func (l Launcher) doCustomPreUpgrade() error {
}
// check if preupgradeFile is executable file
preupgradeFile := filepath.Join(l.cfg.Home, "cosmovisor", l.cfg.CustomPreupgrade)
preupgradeFile := filepath.Join(l.cfg.Home, "cosmovisor", l.cfg.CustomPreUpgrade)
l.logger.Info("looking for COSMOVISOR_CUSTOM_PREUPGRADE file", "file", preupgradeFile)
info, err := os.Stat(preupgradeFile)
if err != nil {
@ -264,8 +264,8 @@ func (l Launcher) doCustomPreUpgrade() error {
func (l *Launcher) doPreUpgrade() error {
counter := 0
for {
if counter > l.cfg.PreupgradeMaxRetries {
return fmt.Errorf("pre-upgrade command failed. reached max attempt of retries - %d", l.cfg.PreupgradeMaxRetries)
if counter > l.cfg.PreUpgradeMaxRetries {
return fmt.Errorf("pre-upgrade command failed. reached max attempt of retries - %d", l.cfg.PreUpgradeMaxRetries)
}
if err := l.executePreUpgradeCmd(); err != nil {

View File

@ -272,7 +272,7 @@ func (s *processTestSuite) TestLaunchProcessWithDownloadsAndMissingPreupgrade()
AllowDownloadBinaries: true,
PollInterval: 100,
UnsafeSkipBackup: true,
CustomPreupgrade: "missing.sh",
CustomPreUpgrade: "missing.sh",
}
logger := log.NewTestLogger(s.T()).With(log.ModuleKey, "cosmovisor")
upgradeFilename := cfg.UpgradeInfoFilePath()
@ -308,7 +308,7 @@ func (s *processTestSuite) TestLaunchProcessWithDownloadsAndPreupgrade() {
AllowDownloadBinaries: true,
PollInterval: 100,
UnsafeSkipBackup: true,
CustomPreupgrade: "preupgrade.sh",
CustomPreUpgrade: "preupgrade.sh",
}
buf := newBuffer() // inspect output using buf.String()
logger := log.NewLogger(buf).With(log.ModuleKey, "cosmovisor")