cosmos-sdk/cosmovisor/args.go

363 lines
10 KiB
Go

package cosmovisor
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/rs/zerolog"
cverrors "github.com/cosmos/cosmos-sdk/cosmovisor/errors"
upgradekeeper "github.com/cosmos/cosmos-sdk/x/upgrade/keeper"
upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types"
)
// environment variable names
const (
EnvHome = "DAEMON_HOME"
EnvName = "DAEMON_NAME"
EnvDownloadBin = "DAEMON_ALLOW_DOWNLOAD_BINARIES"
EnvRestartUpgrade = "DAEMON_RESTART_AFTER_UPGRADE"
EnvSkipBackup = "UNSAFE_SKIP_BACKUP"
EnvDataBackupPath = "DAEMON_DATA_BACKUP_DIR"
EnvInterval = "DAEMON_POLL_INTERVAL"
EnvPreupgradeMaxRetries = "DAEMON_PREUPGRADE_MAX_RETRIES"
)
const (
rootName = "cosmovisor"
genesisDir = "genesis"
upgradesDir = "upgrades"
currentLink = "current"
)
// must be the same as x/upgrade/types.UpgradeInfoFilename
const defaultFilename = "upgrade-info.json"
// Config is the information passed in to control the daemon
type Config struct {
Home string
Name string
AllowDownloadBinaries bool
RestartAfterUpgrade bool
PollInterval time.Duration
UnsafeSkipBackup bool
DataBackupPath string
PreupgradeMaxRetries int
// currently running upgrade
currentUpgrade upgradetypes.Plan
}
// Root returns the root directory where all info lives
func (cfg *Config) Root() string {
return filepath.Join(cfg.Home, rootName)
}
// 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)
}
// UpgradeBin is the path to the binary for the named upgrade
func (cfg *Config) UpgradeBin(upgradeName string) string {
return filepath.Join(cfg.UpgradeDir(upgradeName), "bin", cfg.Name)
}
// UpgradeDir is the directory named upgrade
func (cfg *Config) UpgradeDir(upgradeName string) string {
safeName := url.PathEscape(upgradeName)
return filepath.Join(cfg.BaseUpgradeDir(), safeName)
}
// BaseUpgradeDir is the directory containing the named upgrade directories.
func (cfg *Config) BaseUpgradeDir() string {
return filepath.Join(cfg.Root(), upgradesDir)
}
// UpgradeInfoFilePath is the expected upgrade-info filename created by `x/upgrade/keeper`.
func (cfg *Config) UpgradeInfoFilePath() string {
return filepath.Join(cfg.Home, "data", defaultFilename)
}
// SymLinkToGenesis creates a symbolic link from "./current" to the genesis directory.
func (cfg *Config) SymLinkToGenesis() (string, error) {
genesis := filepath.Join(cfg.Root(), genesisDir)
link := filepath.Join(cfg.Root(), currentLink)
if err := os.Symlink(genesis, link); err != nil {
return "", err
}
// and return the genesis binary
return cfg.GenesisBin(), nil
}
// CurrentBin is the path to the currently selected binary (genesis if no link is set)
// This will resolve the symlink to the underlying directory to make it easier to debug
func (cfg *Config) CurrentBin() (string, error) {
cur := filepath.Join(cfg.Root(), currentLink)
// if nothing here, fallback to genesis
info, err := os.Lstat(cur)
if err != nil {
// Create symlink to the genesis
return cfg.SymLinkToGenesis()
}
// if it is there, ensure it is a symlink
if info.Mode()&os.ModeSymlink == 0 {
// Create symlink to the genesis
return cfg.SymLinkToGenesis()
}
// resolve it
dest, err := os.Readlink(cur)
if err != nil {
// Create symlink to the genesis
return cfg.SymLinkToGenesis()
}
// and return the binary
binpath := filepath.Join(dest, "bin", cfg.Name)
return binpath, nil
}
// GetConfigFromEnv will read the environmental variables into a config
// and then validate it is reasonable
func GetConfigFromEnv() (*Config, error) {
var errs []error
cfg := &Config{
Home: os.Getenv(EnvHome),
Name: os.Getenv(EnvName),
DataBackupPath: os.Getenv(EnvDataBackupPath),
}
if cfg.DataBackupPath == "" {
cfg.DataBackupPath = cfg.Home
}
var err error
if cfg.AllowDownloadBinaries, err = booleanOption(EnvDownloadBin, false); err != nil {
errs = append(errs, err)
}
if cfg.RestartAfterUpgrade, err = booleanOption(EnvRestartUpgrade, true); err != nil {
errs = append(errs, err)
}
if cfg.UnsafeSkipBackup, err = booleanOption(EnvSkipBackup, false); err != nil {
errs = append(errs, err)
}
interval := os.Getenv(EnvInterval)
if interval != "" {
var intervalUInt uint64
intervalUInt, err = strconv.ParseUint(interval, 10, 32)
if err == nil {
cfg.PollInterval = time.Millisecond * time.Duration(intervalUInt)
} else {
cfg.PollInterval, err = time.ParseDuration(interval)
}
switch {
case err != nil:
errs = append(errs, fmt.Errorf("invalid %s: could not parse \"%s\" into either a duration or uint (milliseconds)", EnvInterval, interval))
case cfg.PollInterval <= 0:
errs = append(errs, fmt.Errorf("invalid %s: must be greater than 0", EnvInterval))
}
} else {
cfg.PollInterval = 300 * time.Millisecond
}
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))
}
errs = append(errs, cfg.validate()...)
if len(errs) > 0 {
return nil, cverrors.FlattenErrors(errs...)
}
return cfg, nil
}
// LogConfigOrError logs either the config details or the error.
func LogConfigOrError(logger zerolog.Logger, cfg *Config, err error) {
if cfg == nil && err == nil {
return
}
logger.Info().Msg("Configuration:")
switch {
case err != nil:
cverrors.LogErrors(logger, "configuration errors found", err)
case cfg != nil:
logger.Info().Msg(cfg.DetailString())
}
}
// validate returns an error if this config is invalid.
// it enforces Home/cosmovisor is a valid directory and exists,
// and that Name is set
func (cfg *Config) validate() []error {
var errs []error
if cfg.Name == "" {
errs = append(errs, errors.New(EnvName+" is not set"))
}
switch {
case cfg.Home == "":
errs = append(errs, errors.New(EnvHome+" is not set"))
case !filepath.IsAbs(cfg.Home):
errs = append(errs, errors.New(EnvHome+" must be an absolute path"))
default:
switch info, err := os.Stat(cfg.Root()); {
case err != nil:
errs = append(errs, fmt.Errorf("cannot stat home dir: %w", err))
case !info.IsDir():
errs = append(errs, fmt.Errorf("%s is not a directory", cfg.Root()))
}
}
// check the DataBackupPath
if cfg.UnsafeSkipBackup == true {
return errs
}
// if UnsafeSkipBackup is false, check if the DataBackupPath valid
switch {
case cfg.DataBackupPath == "":
errs = append(errs, errors.New(EnvDataBackupPath + " must not be empty"))
case !filepath.IsAbs(cfg.DataBackupPath):
errs = append(errs, errors.New(cfg.DataBackupPath + " must be an absolute path"))
default:
switch info, err := os.Stat(cfg.DataBackupPath); {
case err != nil:
errs = append(errs, fmt.Errorf("%q must be a valid directory: %w", cfg.DataBackupPath, err))
case !info.IsDir():
errs = append(errs, fmt.Errorf("%q must be a valid directory", cfg.DataBackupPath))
}
}
return errs
}
// SetCurrentUpgrade sets the named upgrade to be the current link, returns error if this binary doesn't exist
func (cfg *Config) SetCurrentUpgrade(u upgradetypes.Plan) error {
// ensure named upgrade exists
bin := cfg.UpgradeBin(u.Name)
if err := EnsureBinary(bin); err != nil {
return err
}
// set a symbolic link
link := filepath.Join(cfg.Root(), currentLink)
safeName := url.PathEscape(u.Name)
upgrade := filepath.Join(cfg.Root(), upgradesDir, safeName)
// remove link if it exists
if _, err := os.Stat(link); err == nil {
os.Remove(link)
}
// point to the new directory
if err := os.Symlink(upgrade, link); err != nil {
return fmt.Errorf("creating current symlink: %w", err)
}
cfg.currentUpgrade = u
f, err := os.Create(filepath.Join(upgrade, upgradekeeper.UpgradeInfoFileName))
if err != nil {
return err
}
bz, err := json.Marshal(u)
if err != nil {
return err
}
if _, err := f.Write(bz); err != nil {
return err
}
return f.Close()
}
func (cfg *Config) UpgradeInfo() upgradetypes.Plan {
if cfg.currentUpgrade.Name != "" {
return cfg.currentUpgrade
}
filename := filepath.Join(cfg.Root(), currentLink, upgradekeeper.UpgradeInfoFileName)
_, err := os.Lstat(filename)
var u upgradetypes.Plan
var bz []byte
if err != nil { // no current directory
goto returnError
}
if bz, err = os.ReadFile(filename); err != nil {
goto returnError
}
if err = json.Unmarshal(bz, &u); err != nil {
goto returnError
}
cfg.currentUpgrade = u
return cfg.currentUpgrade
returnError:
Logger.Error().Err(err).Str("filename", filename).Msg("failed to read")
cfg.currentUpgrade.Name = "_"
return cfg.currentUpgrade
}
// checks and validates env option
func booleanOption(name string, defaultVal bool) (bool, error) {
p := strings.ToLower(os.Getenv(name))
switch p {
case "":
return defaultVal, nil
case "false":
return false, nil
case "true":
return true, nil
}
return false, fmt.Errorf("env variable %q must have a boolean value (\"true\" or \"false\"), got %q", name, p)
}
// DetailString returns a multi-line string with details about this config.
func (cfg Config) DetailString() string {
configEntries := []struct{ name, value string }{
{EnvHome, cfg.Home},
{EnvName, cfg.Name},
{EnvDownloadBin, fmt.Sprintf("%t", cfg.AllowDownloadBinaries)},
{EnvRestartUpgrade, fmt.Sprintf("%t", cfg.RestartAfterUpgrade)},
{EnvInterval, fmt.Sprintf("%s", cfg.PollInterval)},
{EnvSkipBackup, fmt.Sprintf("%t", cfg.UnsafeSkipBackup)},
{EnvDataBackupPath, cfg.DataBackupPath},
{EnvPreupgradeMaxRetries, fmt.Sprintf("%d", cfg.PreupgradeMaxRetries)},
}
derivedEntries := []struct{ name, value string }{
{"Root Dir", cfg.Root()},
{"Upgrade Dir", cfg.BaseUpgradeDir()},
{"Genesis Bin", cfg.GenesisBin()},
{"Monitored File", cfg.UpgradeInfoFilePath()},
{"Data Backup Dir", cfg.DataBackupPath},
}
var sb strings.Builder
sb.WriteString("Configurable Values:\n")
for _, kv := range configEntries {
sb.WriteString(fmt.Sprintf(" %s: %s\n", kv.name, kv.value))
}
sb.WriteString("Derived Values:\n")
dnl := 0
for _, kv := range derivedEntries {
if len(kv.name) > dnl {
dnl = len(kv.name)
}
}
dFmt := fmt.Sprintf(" %%%ds: %%s\n", dnl)
for _, kv := range derivedEntries {
sb.WriteString(fmt.Sprintf(dFmt, kv.name, kv.value))
}
return sb.String()
}