Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
71b692a
feat: load cosmovisor configuration from toml file
akaladarshi Apr 10, 2024
af65025
fix: linter issues
akaladarshi Apr 10, 2024
2cfc0c3
Merge branch 'main' into akaladarshi/load-config-cosmovisor
akaladarshi Apr 11, 2024
7335e53
Merge branch 'main' into akaladarshi/load-config-cosmovisor
akaladarshi Apr 11, 2024
3351c7b
test: add test cases
akaladarshi Apr 11, 2024
2d2da1b
fix: linter errors
akaladarshi Apr 11, 2024
240402e
Merge branch 'main' into akaladarshi/load-config-cosmovisor
akaladarshi Apr 11, 2024
7e84917
Merge branch 'main' into akaladarshi/load-config-cosmovisor
akaladarshi Apr 12, 2024
44a724e
docs: update changelog
akaladarshi Apr 12, 2024
74c8b24
fix: changelog indentation
akaladarshi Apr 12, 2024
13faea8
Merge branch 'main' into akaladarshi/load-config-cosmovisor
akaladarshi Apr 13, 2024
fa973a5
address comments
akaladarshi Apr 14, 2024
03aa6d4
nit: fix typo
akaladarshi Apr 14, 2024
b2f5594
Merge branch 'main' into akaladarshi/load-config-cosmovisor
akaladarshi Apr 19, 2024
75f7c2e
Merge branch 'main' into akaladarshi/load-config-cosmovisor
akaladarshi Apr 20, 2024
7307c76
Merge branch 'main' into akaladarshi/load-config-cosmovisor
akaladarshi Apr 21, 2024
bc91f5a
Merge branch 'main' into akaladarshi/load-config-cosmovisor
akaladarshi Apr 23, 2024
9a9a729
address comments
akaladarshi Apr 23, 2024
5356e55
Merge branch 'main' into akaladarshi/load-config-cosmovisor
akaladarshi Apr 23, 2024
5a7eecc
Merge branch 'main' into akaladarshi/load-config-cosmovisor
akaladarshi Apr 23, 2024
3ea4396
Merge branch 'main' into akaladarshi/load-config-cosmovisor
akaladarshi Apr 29, 2024
6724f0c
Merge branch 'main' into akaladarshi/load-config-cosmovisor
akaladarshi May 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions tools/cosmovisor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ Ref: https://keepachangelog.com/en/1.0.0/

## [Unreleased]

## Features
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure there is a blank line before the "Features" heading to maintain consistency and readability.

+ 
## Features

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
## Features
## Features


* [#19764](https://github.com/cosmos/cosmos-sdk/issues/19764) Use config file for cosmovisor configuration.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure there is a blank line after the "Features" list item to separate it from the next section for better readability.

* [#19764](https://github.com/cosmos/cosmos-sdk/issues/19764) Use config file for cosmovisor configuration.
+ 

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
* [#19764](https://github.com/cosmos/cosmos-sdk/issues/19764) Use config file for cosmovisor configuration.
* [#19764](https://github.com/cosmos/cosmos-sdk/issues/19764) Use config file for cosmovisor configuration.


## Improvements
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure there is a blank line before the "Improvements" heading to maintain consistency and readability.

+ 
## Improvements

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
## Improvements
## Improvements


* [#19995](https://github.com/cosmos/cosmos-sdk/pull/19995):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure there is a blank line after the "Improvements" list item to separate it from the next section for better readability.

* [#19995](https://github.com/cosmos/cosmos-sdk/pull/19995):
+ 

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
* [#19995](https://github.com/cosmos/cosmos-sdk/pull/19995):
* [#19995](https://github.com/cosmos/cosmos-sdk/pull/19995):

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice description here, could you as well update the README with almost a copy paste of that :D

* Add `--export-config` flag to provide `config.toml` path to initialise configuration in `cosmovisor init`.
* Set `CONFIG` environment variable to provide `config.toml` path to load the configuration in `cosmovisor run`
* Add `--config` flag to provide `config.toml` path to the configuration file in `cosmovisor add-upgrade`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adjust the indentation of the unordered list items to align with markdown standards (4 spaces instead of 2).

-    * Add `--export-config` flag to provide `config.toml` path to initialise configuration in `cosmovisor init`.
+        * Add `--export-config` flag to provide `config.toml` path to initialise configuration in `cosmovisor init`.
-    * Set `CONFIG` environment variable to provide `config.toml` path to load the configuration in `cosmovisor run`
+        * Set `CONFIG` environment variable to provide `config.toml` path to load the configuration in `cosmovisor run`
-    * Add `--config` flag to provide `config.toml` path to the configuration file in `cosmovisor add-upgrade`.
+        * Add `--config` flag to provide `config.toml` path to the configuration file in `cosmovisor add-upgrade`.

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
* Add `--export-config` flag to provide `config.toml` path to initialise configuration in `cosmovisor init`.
* Set `CONFIG` environment variable to provide `config.toml` path to load the configuration in `cosmovisor run`
* Add `--config` flag to provide `config.toml` path to the configuration file in `cosmovisor add-upgrade`.
* Add `--export-config` flag to provide `config.toml` path to initialise configuration in `cosmovisor init`.
* Set `CONFIG` environment variable to provide `config.toml` path to load the configuration in `cosmovisor run`
* Add `--config` flag to provide `config.toml` path to the configuration file in `cosmovisor add-upgrade`.


## v1.5.0 - 2023-07-17

## Features
Expand Down
174 changes: 151 additions & 23 deletions tools/cosmovisor/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@ 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"
)

var ErrEmptyConfigENV = errors.New("config env variable not set or empty")

// environment variable names
const (
EnvHome = "DAEMON_HOME"
Expand All @@ -42,26 +47,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"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have the toml keys lower-case and kebab-case? This looks odd

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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 +80,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 +158,51 @@ func (cfg *Config) CurrentBin() (string, error) {
return binpath, nil
}

// GetConfigFromFile will read the configuration from the file at the given path.
// It will return an error if the file does not exist or if the configuration is invalid.
// If ENV variables are set, they will override the values in the file.
func GetConfigFromFile(filePath string) (*Config, error) {
if filePath == "" {
return nil, ErrEmptyConfigENV
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should return an error, that will be a behavior change for existing users. Maybe we should use a temporary empty file? Quite hacky, but at least it will preserve the current behavior for users that won't use the config file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense.

Updated. Now if the filePath is not provided instead of returning an error it will try to load config from env variables

}

// 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 +211,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 +278,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 +413,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 +440,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 +454,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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getTimeFormatOption function should handle an empty string case more gracefully by returning a default or previously set value instead of an empty string, which might not be a valid time format.

switch val {
case "layout":
return time.Layout, nil
Expand Down Expand Up @@ -432,6 +496,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 +541,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 +575,35 @@ func (cfg Config) DetailString() string {
}
return sb.String()
}

// Export exports the configuration to a file at the given path.
func (cfg Config) Export(path string) (string, error) {
// if path is empty, use the default path
if path == "" {
path = cfg.DefaultCfgPath()
}

// ensure the path has proper extension
if !strings.HasSuffix(path, cfgExtension) {
return "", fmt.Errorf("invalid file extension must have %s extension", cfgExtension)
}

// 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
}