-
Notifications
You must be signed in to change notification settings - Fork 3.8k
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
Changes from 7 commits
71b692a
af65025
2cfc0c3
7335e53
3351c7b
2d2da1b
240402e
7e84917
44a724e
74c8b24
13faea8
fa973a5
03aa6d4
b2f5594
75f7c2e
7307c76
bc91f5a
9a9a729
5356e55
5a7eecc
3ea4396
6724f0c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
|
@@ -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_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 +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) | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make sense. Updated. Now if the |
||
} | ||
|
||
// 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 +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 == "" { | ||
|
@@ -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)) | ||
} | ||
|
||
|
@@ -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 | ||
|
@@ -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 { | ||
|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
switch val { | ||
case "layout": | ||
return time.Layout, nil | ||
|
@@ -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 }{ | ||
|
@@ -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)}, | ||
} | ||
|
||
|
@@ -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 | ||
} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.