Skip to content

Commit

Permalink
feat(cosmovisor): load cosmovisor configuration from toml file (#19995)
Browse files Browse the repository at this point in the history
  • Loading branch information
akaladarshi authored May 30, 2024
1 parent 90cbb02 commit 465410c
Show file tree
Hide file tree
Showing 13 changed files with 537 additions and 94 deletions.
12 changes: 12 additions & 0 deletions tools/cosmovisor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
185 changes: 162 additions & 23 deletions tools/cosmovisor/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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) {
Expand All @@ -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 == "" {
Expand Down Expand Up @@ -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))
}

Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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 }{
Expand All @@ -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)},
}

Expand Down Expand Up @@ -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"
}
Loading

0 comments on commit 465410c

Please sign in to comment.