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

Merged
merged 22 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from 12 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
12 changes: 12 additions & 0 deletions tools/cosmovisor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ 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

Copy link
Member

Choose a reason for hiding this comment

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

I'll do this in a follow-up then, as I am touching cosmovisor. Merging now.

* `init command` writes the configuration to the config file only at the default path `DAEMON_HOME/cosmovisor/config.toml`.
* 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 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.
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 list items to align with Markdown standards.

-    * `init command` writes the configuration to the config file only at the default path `DAEMON_HOME/cosmovisor/config.toml`.
+  * `init command` writes the configuration to the config file only at the default path `DAEMON_HOME/cosmovisor/config.toml`.
-    * 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 root command used by `add-upgrade` and `config` subcommands.
+  * Add `--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.
+  * `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.

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
* `init command` writes the configuration to the config file only at the default path `DAEMON_HOME/cosmovisor/config.toml`.
* 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 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.
* `init command` writes the configuration to the config file only at the default path `DAEMON_HOME/cosmovisor/config.toml`.
* 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 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
187 changes: 164 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"`
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_timeforamt_logs" mapstructure:"cosmovisor_timeforamt_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"`
akaladarshi marked this conversation as resolved.
Show resolved Hide resolved

// 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.
// 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 +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,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
}
Comment on lines +577 to +609
Copy link
Contributor

Choose a reason for hiding this comment

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

In the Export function, the confirmation prompt logic could be improved. Consider moving the prompt into a separate function to clean up the Export function and make it more readable.

- 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
- }
+ if fileExists(path) {
+     if !confirmOverwrite(path) {
+         cfg.Logger(os.Stdout).Info("file already exists, not overriding")
+         return path, nil
+     }
+ }

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
// 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
}
// 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 fileExists(path) {
if !confirmOverwrite(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"
}