diff --git a/tools/cosmovisor/CHANGELOG.md b/tools/cosmovisor/CHANGELOG.md index afd128124577..7067ab11df8d 100644 --- a/tools/cosmovisor/CHANGELOG.md +++ b/tools/cosmovisor/CHANGELOG.md @@ -38,10 +38,10 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## Features +* [#16413](https://github.com/cosmos/cosmos-sdk/issues/16413) Add `cosmovisor pre-upgrade` command to manually add an upgrade to cosmovisor. * [#16573](https://github.com/cosmos/cosmos-sdk/pull/16573) Extend `cosmovisor` configuration with new log format options * [#16550](https://github.com/cosmos/cosmos-sdk/pull/16550) Add COSMOVISOR_CUSTOM_PREUPGRADE to cosmovisor to execute custom pre-upgrade scripts (separate from daemon pre-upgrade). * [#15361](https://github.com/cosmos/cosmos-sdk/pull/15361) Add `cosmovisor config` command to display the configuration used by cosmovisor. -* [#12457](https://github.com/cosmos/cosmos-sdk/issues/12457) Add `cosmovisor pre-upgrade` command to manually add an upgrade to cosmovisor. ## Improvements diff --git a/tools/cosmovisor/README.md b/tools/cosmovisor/README.md index 6d6fbb26bf72..013a7cd466f0 100644 --- a/tools/cosmovisor/README.md +++ b/tools/cosmovisor/README.md @@ -52,10 +52,10 @@ To install the latest version of `cosmovisor`, run the following command: go install cosmossdk.io/tools/cosmovisor/cmd/cosmovisor@latest ``` -To install a previous version, you can specify the version. IMPORTANT: Chains that use Cosmos SDK v0.44.3 or earlier (eg v0.44.2) and want to use auto-download feature MUST use `cosmovisor v0.1.0` +To install a previous version, you can specify the version: ```shell -go install github.com/cosmos/cosmos-sdk/cosmovisor/cmd/cosmovisor@v0.1.0 +go install github.com/cosmos/cosmos-sdk/cosmovisor/cmd/cosmovisor@v1.5.0 ``` Run `cosmovisor version` to check the cosmovisor version. @@ -367,7 +367,7 @@ Open a new terminal window and submit an upgrade proposal along with a deposit a **>= v0.50+**: ```shell -./build/simd tx upgrade software-upgrade test1 --title upgrade --summary upgrade --upgrade-height 200 --from validator --yes +./build/simd tx upgrade software-upgrade test1 --title upgrade --summary upgrade --upgrade-height 200 --upgrade-info "{}" --no-validate --from validator --yes ./build/simd tx gov deposit 1 10000000stake --from validator --yes ./build/simd tx gov vote 1 yes --from validator --yes ``` diff --git a/tools/cosmovisor/args.go b/tools/cosmovisor/args.go index e16cc95c0a61..443580229eb9 100644 --- a/tools/cosmovisor/args.go +++ b/tools/cosmovisor/args.go @@ -12,8 +12,6 @@ import ( "strings" "time" - "github.com/rs/zerolog" - "cosmossdk.io/log" "cosmossdk.io/x/upgrade/plan" upgradetypes "cosmossdk.io/x/upgrade/types" @@ -44,9 +42,6 @@ const ( 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 @@ -96,7 +91,7 @@ func (cfg *Config) BaseUpgradeDir() string { // UpgradeInfoFilePath is the expected upgrade-info filename created by `x/upgrade/keeper`. func (cfg *Config) UpgradeInfoFilePath() string { - return filepath.Join(cfg.Home, "data", defaultFilename) + return filepath.Join(cfg.Home, "data", upgradetypes.UpgradeInfoFilename) } // SymLinkToGenesis creates a symbolic link from "./current" to the genesis directory. @@ -224,7 +219,7 @@ func (cfg *Config) Logger(dst io.Writer) log.Logger { var logger log.Logger if cfg.DisableLogs { - logger = log.NewCustomLogger(zerolog.Nop()) + logger = log.NewNopLogger() } else { logger = log.NewLogger(dst, log.ColorOption(cfg.ColorLogs), diff --git a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go index 683660a72381..e914b1d4bc47 100644 --- a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go +++ b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go @@ -1,13 +1,16 @@ package main import ( + "encoding/json" "fmt" "os" "path" + "strings" "github.com/spf13/cobra" "cosmossdk.io/tools/cosmovisor" + upgradetypes "cosmossdk.io/x/upgrade/types" ) func NewAddUpgradeCmd() *cobra.Command { @@ -19,7 +22,8 @@ func NewAddUpgradeCmd() *cobra.Command { RunE: AddUpgrade, } - addUpgrade.Flags().Bool(cosmovisor.FlagForce, false, "overwrite existing upgrade binary") + addUpgrade.Flags().Bool(cosmovisor.FlagForce, false, "overwrite existing upgrade binary / upgrade-info.json file") + addUpgrade.Flags().Int64(cosmovisor.FlagUpgradeHeight, 0, "define a height at which to upgrade the binary automatically (without governance proposal)") return addUpgrade } @@ -33,7 +37,7 @@ func AddUpgrade(cmd *cobra.Command, args []string) error { logger := cfg.Logger(os.Stdout) - upgradeName := args[0] + upgradeName := strings.ToLower(args[0]) if len(upgradeName) == 0 { return fmt.Errorf("upgrade name cannot be empty") } @@ -59,22 +63,55 @@ func AddUpgrade(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to read binary: %w", err) } - if _, err := os.Stat(cfg.UpgradeBin(upgradeName)); err == nil { - if force, _ := cmd.Flags().GetBool(cosmovisor.FlagForce); !force { - return fmt.Errorf("upgrade binary already exists at %s", cfg.UpgradeBin(upgradeName)) + force, err := cmd.Flags().GetBool(cosmovisor.FlagForce) + if err != nil { + return fmt.Errorf("failed to get force flag: %w", err) + } + + if err := saveOrAbort(cfg.UpgradeBin(upgradeName), executableData, force); err != nil { + return err + } + + logger.Info(fmt.Sprintf("Using %s for %s upgrade", executablePath, upgradeName)) + logger.Info(fmt.Sprintf("Upgrade binary located at %s", cfg.UpgradeBin(upgradeName))) + + if upgradeHeight, err := cmd.Flags().GetInt64(cosmovisor.FlagUpgradeHeight); err != nil { + return fmt.Errorf("failed to get upgrade-height flag: %w", err) + } else if upgradeHeight > 0 { + plan := upgradetypes.Plan{Name: upgradeName, Height: upgradeHeight} + if err := plan.ValidateBasic(); err != nil { + panic(fmt.Errorf("something is wrong with cosmovisor: %w", err)) + } + + // create upgrade-info.json file + planData, err := json.Marshal(plan) + if err != nil { + return fmt.Errorf("failed to marshal upgrade plan: %w", err) } - logger.Info(fmt.Sprintf("Overwriting %s for %s upgrade", executablePath, upgradeName)) + if err := saveOrAbort(cfg.UpgradeInfoFilePath(), planData, force); err != nil { + return err + } + + logger.Info(fmt.Sprintf("%s created, %s upgrade binary will switch at height %d", upgradetypes.UpgradeInfoFilename, upgradeName, upgradeHeight)) + } + + return nil +} + +// saveOrAbort saves data to path or aborts if file exists and force is false +func saveOrAbort(path string, data []byte, force bool) error { + if _, err := os.Stat(path); err == nil { + if !force { + return fmt.Errorf("file already exists at %s", path) + } } else if !os.IsNotExist(err) { - return fmt.Errorf("failed to check if upgrade binary exists: %w", err) + return fmt.Errorf("failed to check if file exists: %w", err) } - if err := os.WriteFile(cfg.UpgradeBin(upgradeName), executableData, 0o600); err != nil { + if err := os.WriteFile(path, data, 0o600); err != nil { return fmt.Errorf("failed to write binary to location: %w", err) } - logger.Info(fmt.Sprintf("Using %s for %s upgrade", executablePath, upgradeName)) - logger.Info(fmt.Sprintf("Upgrade binary located at %s", cfg.UpgradeBin(upgradeName))) - return nil } diff --git a/tools/cosmovisor/cmd/cosmovisor/init.go b/tools/cosmovisor/cmd/cosmovisor/init.go index afcbce43e905..9b08529e647e 100644 --- a/tools/cosmovisor/cmd/cosmovisor/init.go +++ b/tools/cosmovisor/cmd/cosmovisor/init.go @@ -16,9 +16,10 @@ import ( ) var initCmd = &cobra.Command{ - Use: "init ", - Short: "Initialize a cosmovisor daemon home directory.", - Args: cobra.ExactArgs(1), + Use: "init ", + Short: "Initialize a cosmovisor daemon home directory.", + Args: cobra.ExactArgs(1), + SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { return InitializeCosmovisor(nil, args) }, diff --git a/tools/cosmovisor/cmd/cosmovisor/init_test.go b/tools/cosmovisor/cmd/cosmovisor/init_test.go index 40df7d153e75..89f39d0d8c6c 100644 --- a/tools/cosmovisor/cmd/cosmovisor/init_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/init_test.go @@ -9,7 +9,6 @@ import ( "testing" "time" - "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -244,8 +243,7 @@ func (p *BufferedPipe) panicIfStarted(msg string) { func (s *InitTestSuite) NewCapturingLogger() (*BufferedPipe, log.Logger) { bufferedStdOut, err := StartNewBufferedPipe("stdout", os.Stdout) s.Require().NoError(err, "creating stdout buffered pipe") - output := zerolog.ConsoleWriter{Out: bufferedStdOut, TimeFormat: time.RFC3339Nano} - logger := log.NewCustomLogger(zerolog.New(output).With().Str("module", "cosmovisor").Timestamp().Logger()) + logger := log.NewLogger(bufferedStdOut, log.ColorOption(false), log.TimeFormatOption(time.RFC3339Nano)).With(log.ModuleKey, "cosmovisor") return &bufferedStdOut, logger } diff --git a/tools/cosmovisor/cmd/cosmovisor/version.go b/tools/cosmovisor/cmd/cosmovisor/version.go index df1c3cd1520a..fa9778ea54d8 100644 --- a/tools/cosmovisor/cmd/cosmovisor/version.go +++ b/tools/cosmovisor/cmd/cosmovisor/version.go @@ -13,10 +13,11 @@ import ( func NewVersionCmd() *cobra.Command { versionCmd := &cobra.Command{ - Use: "version", - Short: "Display cosmovisor and APP version.", + Use: "version", + Short: "Display cosmovisor and APP version.", + SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - noAppVersion, _ := cmd.Flags().GetBool(cosmovisor.FlagNoAppVersion) + noAppVersion, _ := cmd.Flags().GetBool(cosmovisor.FlagCosmovisorOnly) if val, err := cmd.Flags().GetString(cosmovisor.FlagOutput); val == "json" && err == nil { return printVersionJSON(cmd, args, noAppVersion) } @@ -26,7 +27,7 @@ func NewVersionCmd() *cobra.Command { } versionCmd.Flags().StringP(cosmovisor.FlagOutput, "o", "text", "Output format (text|json)") - versionCmd.Flags().Bool(cosmovisor.FlagNoAppVersion, false, "Don't print APP version") + versionCmd.Flags().Bool(cosmovisor.FlagCosmovisorOnly, false, "Print cosmovisor version only") return versionCmd } diff --git a/tools/cosmovisor/flags.go b/tools/cosmovisor/flags.go index 76a48d20dad2..73c17b9247dd 100644 --- a/tools/cosmovisor/flags.go +++ b/tools/cosmovisor/flags.go @@ -3,6 +3,7 @@ package cosmovisor const ( FlagOutput = "output" FlagSkipUpgradeHeight = "unsafe-skip-upgrades" - FlagNoAppVersion = "no-app-version" + FlagCosmovisorOnly = "cosmovisor-only" FlagForce = "force" + FlagUpgradeHeight = "upgrade-height" ) diff --git a/tools/cosmovisor/go.mod b/tools/cosmovisor/go.mod index 74ce1840eec3..41528b76becb 100644 --- a/tools/cosmovisor/go.mod +++ b/tools/cosmovisor/go.mod @@ -6,7 +6,6 @@ require ( cosmossdk.io/log v1.1.0 cosmossdk.io/x/upgrade v0.0.0-20230614103911-b3da8bb4e801 github.com/otiai10/copy v1.12.0 - github.com/rs/zerolog v1.29.1 github.com/spf13/cobra v1.7.0 github.com/stretchr/testify v1.8.4 ) @@ -133,6 +132,7 @@ require ( github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rs/cors v1.8.3 // indirect + github.com/rs/zerolog v1.29.1 // indirect github.com/sasha-s/go-deadlock v0.3.1 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect diff --git a/tools/cosmovisor/process.go b/tools/cosmovisor/process.go index 24d0db67241e..9dcf219b3c4a 100644 --- a/tools/cosmovisor/process.go +++ b/tools/cosmovisor/process.go @@ -14,7 +14,6 @@ import ( "time" "github.com/otiai10/copy" - "github.com/rs/zerolog" "cosmossdk.io/log" "cosmossdk.io/x/upgrade/plan" @@ -22,19 +21,18 @@ import ( ) type Launcher struct { - logger *zerolog.Logger + logger log.Logger cfg *Config fw *fileWatcher } func NewLauncher(logger log.Logger, cfg *Config) (Launcher, error) { - fw, err := newUpgradeFileWatcher(logger, cfg.UpgradeInfoFilePath(), cfg.PollInterval) + fw, err := newUpgradeFileWatcher(cfg, logger) if err != nil { return Launcher{}, err } - zl := logger.Impl().(*zerolog.Logger) - return Launcher{logger: zl, cfg: cfg, fw: fw}, nil + return Launcher{logger: logger, cfg: cfg, fw: fw}, nil } // Run launches the app in a subprocess and returns when the subprocess (app) @@ -50,7 +48,7 @@ func (l Launcher) Run(args []string, stdout, stderr io.Writer) (bool, error) { return false, fmt.Errorf("current binary is invalid: %w", err) } - l.logger.Info().Str("path", bin).Strs("args", args).Msg("running app") + l.logger.Info("running app", "path", bin, "args", args) cmd := exec.Command(bin, args...) cmd.Stdout = stdout cmd.Stderr = stderr @@ -63,7 +61,8 @@ func (l Launcher) Run(args []string, stdout, stderr io.Writer) (bool, error) { go func() { sig := <-sigs if err := cmd.Process.Signal(sig); err != nil { - l.logger.Fatal().Err(err).Str("bin", bin).Msg("terminated") + l.logger.Error("terminated", "error", err, "bin", bin) + os.Exit(1) } }() @@ -82,7 +81,7 @@ func (l Launcher) Run(args []string, stdout, stderr io.Writer) (bool, error) { return false, err } - if err := UpgradeBinary(log.NewCustomLogger(*l.logger), l.cfg, l.fw.currentInfo); err != nil { + if err := UpgradeBinary(l.logger, l.cfg, l.fw.currentInfo); err != nil { return false, err } @@ -100,13 +99,14 @@ func (l Launcher) Run(args []string, stdout, stderr io.Writer) (bool, error) { // When it returns, the process (app) is finished. // // It returns (true, nil) if an upgrade should be initiated (and we killed the process) -// It returns (false, err) if the process died by itself, or there was an issue reading the upgrade-info file. +// It returns (false, err) if the process died by itself // It returns (false, nil) if the process exited normally without triggering an upgrade. This is very unlikely -// to happened with "start" but may happened with short-lived commands like `gaiad export ...` +// to happen with "start" but may happen with short-lived commands like `simd export ...` func (l Launcher) WaitForUpgradeOrExit(cmd *exec.Cmd) (bool, error) { currentUpgrade, err := l.cfg.UpgradeInfo() if err != nil { - l.logger.Error().Err(err) + // upgrade info not found do nothing + currentUpgrade = upgradetypes.Plan{} } cmdDone := make(chan error) @@ -117,7 +117,7 @@ func (l Launcher) WaitForUpgradeOrExit(cmd *exec.Cmd) (bool, error) { select { case <-l.fw.MonitorUpdate(currentUpgrade): // upgrade - kill the process and restart - l.logger.Info().Msg("daemon shutting down in an attempt to restart") + l.logger.Info("daemon shutting down in an attempt to restart") _ = cmd.Process.Kill() case err := <-cmdDone: l.fw.Stop() @@ -139,7 +139,7 @@ func (l Launcher) doBackup() error { if !l.cfg.UnsafeSkipBackup { // check if upgrade-info.json is not empty. var uInfo upgradetypes.Plan - upgradeInfoFile, err := os.ReadFile(filepath.Join(l.cfg.Home, "data", "upgrade-info.json")) + upgradeInfoFile, err := os.ReadFile(l.cfg.UpgradeInfoFilePath()) if err != nil { return fmt.Errorf("error while reading upgrade-info.json: %w", err) } @@ -157,7 +157,7 @@ func (l Launcher) doBackup() error { stStr := fmt.Sprintf("%d-%d-%d", st.Year(), st.Month(), st.Day()) dst := filepath.Join(l.cfg.DataBackupPath, fmt.Sprintf("data"+"-backup-%s", stStr)) - l.logger.Info().Time("backup start time", st).Msg("starting to take backup of data directory") + l.logger.Info("starting to take backup of data directory", "backup start time", st) // copy the $DAEMON_HOME/data to a backup dir if err = copy.Copy(filepath.Join(l.cfg.Home, "data"), dst); err != nil { @@ -166,38 +166,39 @@ func (l Launcher) doBackup() error { // backup is done, lets check endtime to calculate total time taken for backup process et := time.Now() - l.logger.Info().Str("backup saved at", dst).Time("backup completion time", et).TimeDiff("time taken to complete backup", et, st).Msg("backup completed") + l.logger.Info("backup completed", "backup saved at", dst, "backup completion time", et, "time taken to complete backup", et.Sub(st)) } return nil } +// doCustomPreUpgrade executes the custom preupgrade script if provided. func (l Launcher) doCustomPreUpgrade() error { if l.cfg.CustomPreupgrade == "" { return nil } // check if upgrade-info.json is not empty. - var uInfo upgradetypes.Plan + var upgradePlan upgradetypes.Plan upgradeInfoFile, err := os.ReadFile(l.cfg.UpgradeInfoFilePath()) if err != nil { return fmt.Errorf("error while reading upgrade-info.json: %w", err) } - if err = json.Unmarshal(upgradeInfoFile, &uInfo); err != nil { + if err = json.Unmarshal(upgradeInfoFile, &upgradePlan); err != nil { return err } - if err = uInfo.ValidateBasic(); err != nil { + if err = upgradePlan.ValidateBasic(); err != nil { return fmt.Errorf("invalid upgrade plan: %w", err) } // check if preupgradeFile is executable file preupgradeFile := filepath.Join(l.cfg.Home, "cosmovisor", l.cfg.CustomPreupgrade) - l.logger.Info().Str("Looking for COSMOVISOR_CUSTOM_PREUPGRADE file ", preupgradeFile) + l.logger.Info("looking for COSMOVISOR_CUSTOM_PREUPGRADE file", "file", preupgradeFile) info, err := os.Stat(preupgradeFile) if err != nil { - l.logger.Error().Str("file", preupgradeFile).Msg("COSMOVISOR_CUSTOM_PREUPGRADE file missing") + l.logger.Error("COSMOVISOR_CUSTOM_PREUPGRADE file missing", "file", preupgradeFile) return err } if !info.Mode().IsRegular() { @@ -212,20 +213,20 @@ func (l Launcher) doCustomPreUpgrade() error { newMode := oldMode | 0o100 if oldMode != newMode { if err := os.Chmod(preupgradeFile, newMode); err != nil { - l.logger.Info().Msg("COSMOVISOR_CUSTOM_PREUPGRADE could not add execute permission") + l.logger.Info("COSMOVISOR_CUSTOM_PREUPGRADE could not add execute permission") return fmt.Errorf("COSMOVISOR_CUSTOM_PREUPGRADE could not add execute permission") } } // Run preupgradeFile - cmd := exec.Command(preupgradeFile, uInfo.Name, fmt.Sprintf("%d", uInfo.Height)) + cmd := exec.Command(preupgradeFile, upgradePlan.Name, fmt.Sprintf("%d", upgradePlan.Height)) cmd.Dir = l.cfg.Home result, err := cmd.Output() if err != nil { return err } - l.logger.Info().Str("command", preupgradeFile).Str("argv1", uInfo.Name).Str("argv2", fmt.Sprintf("%d", uInfo.Height)).Bytes("result", result).Msg("COSMOVISOR_CUSTOM_PREUPGRADE result") + l.logger.Info("COSMOVISOR_CUSTOM_PREUPGRADE result", "command", preupgradeFile, "argv1", upgradePlan.Name, "argv2", fmt.Sprintf("%d", upgradePlan.Height), "result", result) return nil } @@ -245,17 +246,17 @@ func (l *Launcher) doPreUpgrade() error { switch err.(*exec.ExitError).ProcessState.ExitCode() { case 1: - l.logger.Info().Msg("pre-upgrade command does not exist. continuing the upgrade.") + l.logger.Info("pre-upgrade command does not exist. continuing the upgrade.") return nil case 30: return fmt.Errorf("pre-upgrade command failed : %w", err) case 31: - l.logger.Error().Err(err).Int("attempt", counter).Msg("pre-upgrade command failed. retrying") + l.logger.Error("pre-upgrade command failed. retrying", "error", err, "attempt", counter) continue } } - l.logger.Info().Msg("pre-upgrade successful. continuing the upgrade.") + l.logger.Info("pre-upgrade successful. continuing the upgrade.") return nil } } @@ -273,7 +274,7 @@ func (l *Launcher) executePreUpgradeCmd() error { return err } - l.logger.Info().Bytes("result", result).Msg("pre-upgrade result") + l.logger.Info("pre-upgrade result", "result", result) return nil } diff --git a/tools/cosmovisor/process_test.go b/tools/cosmovisor/process_test.go index cbecd1372197..57da5ff7fab4 100644 --- a/tools/cosmovisor/process_test.go +++ b/tools/cosmovisor/process_test.go @@ -93,7 +93,6 @@ func (s *processTestSuite) TestLaunchProcessWithRestartDelay() { upgradeFile := cfg.UpgradeInfoFilePath() start := time.Now() - doUpgrade, err := launcher.Run([]string{"foo", "bar", "1234", upgradeFile}, stdout, stderr) require.NoError(err) require.True(doUpgrade) @@ -115,8 +114,7 @@ func (s *processTestSuite) TestLaunchProcessWithDownloads() { require := s.Require() home := copyTestData(s.T(), "download") cfg := &cosmovisor.Config{Home: home, Name: "autod", AllowDownloadBinaries: true, PollInterval: 100, UnsafeSkipBackup: true} - buf := newBuffer() // inspect output using buf.String() - logger := log.NewLogger(buf).With(log.ModuleKey, "cosmovisor") + logger := log.NewTestLogger(s.T()).With(log.ModuleKey, "cosmovisor") upgradeFilename := cfg.UpgradeInfoFilePath() // should run the genesis binary and produce expected output @@ -130,7 +128,6 @@ func (s *processTestSuite) TestLaunchProcessWithDownloads() { stdout, stderr := newBuffer(), newBuffer() args := []string{"some", "args", upgradeFilename} doUpgrade, err := launcher.Run(args, stdout, stderr) - require.NoError(err) require.True(doUpgrade) require.Equal("", stderr.String()) @@ -180,14 +177,14 @@ func (s *processTestSuite) TestLaunchProcessWithDownloadsAndMissingPreupgrade() require := s.Require() home := copyTestData(s.T(), "download") cfg := &cosmovisor.Config{ - Home: home, Name: "autod", + Home: home, + Name: "autod", AllowDownloadBinaries: true, PollInterval: 100, UnsafeSkipBackup: true, CustomPreupgrade: "missing.sh", } - buf := newBuffer() // inspect output using buf.String() - logger := log.NewLogger(buf).With(log.ModuleKey, "cosmovisor") + logger := log.NewTestLogger(s.T()).With(log.ModuleKey, "cosmovisor") upgradeFilename := cfg.UpgradeInfoFilePath() // should run the genesis binary and produce expected output diff --git a/tools/cosmovisor/scanner.go b/tools/cosmovisor/scanner.go index 435b10f3ef5b..c64445a9cc1e 100644 --- a/tools/cosmovisor/scanner.go +++ b/tools/cosmovisor/scanner.go @@ -5,57 +5,59 @@ import ( "errors" "fmt" "os" + "os/exec" "path/filepath" + "strconv" "strings" "time" - "github.com/rs/zerolog" - "cosmossdk.io/log" upgradetypes "cosmossdk.io/x/upgrade/types" ) type fileWatcher struct { - logger log.Logger - - // full path to a watched file - filename string + filename string // full path to a watched file interval time.Duration + currentBin string currentInfo upgradetypes.Plan lastModTime time.Time cancel chan bool ticker *time.Ticker - needsUpdate bool + needsUpdate bool initialized bool } -func newUpgradeFileWatcher(logger log.Logger, filename string, interval time.Duration) (*fileWatcher, error) { +func newUpgradeFileWatcher(cfg *Config, logger log.Logger) (*fileWatcher, error) { + filename := cfg.UpgradeInfoFilePath() if filename == "" { return nil, errors.New("filename undefined") } filenameAbs, err := filepath.Abs(filename) if err != nil { - return nil, - fmt.Errorf("invalid path; %s must be a valid file path: %w", filename, err) + return nil, fmt.Errorf("invalid path: %s must be a valid file path: %w", filename, err) } dirname := filepath.Dir(filename) - info, err := os.Stat(dirname) - if err != nil || !info.IsDir() { - return nil, fmt.Errorf("invalid path; %s must be an existing directory: %w", dirname, err) + if info, err := os.Stat(dirname); err != nil || !info.IsDir() { + return nil, fmt.Errorf("invalid path: %s must be an existing directory: %w", dirname, err) + } + + bin, err := cfg.CurrentBin() + if err != nil { + return nil, fmt.Errorf("error creating symlink to genesis: %w", err) } return &fileWatcher{ - logger: logger, + currentBin: bin, filename: filenameAbs, - interval: interval, + interval: cfg.PollInterval, currentInfo: upgradetypes.Plan{}, lastModTime: time.Time{}, cancel: make(chan bool), - ticker: time.NewTicker(interval), + ticker: time.NewTicker(cfg.PollInterval), needsUpdate: false, initialized: false, }, nil @@ -65,9 +67,9 @@ func (fw *fileWatcher) Stop() { close(fw.cancel) } -// pools the filesystem to check for new upgrade currentInfo. currentName is the name -// of currently running upgrade. The check is rejected if it finds an upgrade with the same -// name. +// MonitorUpdate pools the filesystem to check for new upgrade currentInfo. +// currentName is the name of currently running upgrade. The check is rejected if it finds +// an upgrade with the same name. func (fw *fileWatcher) MonitorUpdate(currentUpgrade upgradetypes.Plan) <-chan struct{} { fw.ticker.Reset(fw.interval) done := make(chan struct{}) @@ -112,8 +114,12 @@ func (fw *fileWatcher) CheckUpdate(currentUpgrade upgradetypes.Plan) bool { info, err := parseUpgradeInfoFile(fw.filename) if err != nil { - zl := fw.logger.Impl().(*zerolog.Logger) - zl.Fatal().Err(err).Msg("failed to parse upgrade info file") + panic(fmt.Errorf("failed to parse upgrade info file: %w", err)) + } + + // file exist but too early in height + currentHeight, _ := fw.checkHeight() + if currentHeight != 0 && currentHeight < info.Height { return false } @@ -142,27 +148,60 @@ func (fw *fileWatcher) CheckUpdate(currentUpgrade upgradetypes.Plan) bool { return false } -func parseUpgradeInfoFile(filename string) (upgradetypes.Plan, error) { - var ui upgradetypes.Plan +// checkHeight checks if the current block height +func (fw *fileWatcher) checkHeight() (int64, error) { + // TODO(@julienrbrt) use `if !testing.Testing()` from Go 1.22 + // The tests from `process_test.go`, which run only on linux, are failing when using `autod` that is a bash script. + // In production, the binary will always be an application with a status command, but in tests it isn't not. + if strings.HasSuffix(os.Args[0], ".test") { + return 0, nil + } + + result, err := exec.Command(fw.currentBin, "status").Output() //nolint:gosec // we want to execute the status command + if err != nil { + return 0, err + } + + type response struct { + SyncInfo struct { + LatestBlockHeight string `json:"latest_block_height"` + } `json:"SyncInfo"` + } - f, err := os.Open(filename) + var resp response + if err := json.Unmarshal(result, &resp); err != nil { + return 0, err + } + + if resp.SyncInfo.LatestBlockHeight == "" { + return 0, errors.New("latest block height is empty") + } + + return strconv.ParseInt(resp.SyncInfo.LatestBlockHeight, 10, 64) +} + +func parseUpgradeInfoFile(filename string) (upgradetypes.Plan, error) { + f, err := os.ReadFile(filename) if err != nil { return upgradetypes.Plan{}, err } - defer f.Close() - d := json.NewDecoder(f) - if err := d.Decode(&ui); err != nil { + if len(f) == 0 { + return upgradetypes.Plan{}, errors.New("empty upgrade-info.json") + } + + var upgradePlan upgradetypes.Plan + if err := json.Unmarshal(f, &upgradePlan); err != nil { return upgradetypes.Plan{}, err } // required values must be set - if ui.Height <= 0 || ui.Name == "" { - return upgradetypes.Plan{}, fmt.Errorf("invalid upgrade-info.json content; name and height must be not empty; got: %v", ui) + if err := upgradePlan.ValidateBasic(); err != nil { + return upgradetypes.Plan{}, fmt.Errorf("invalid upgrade-info.json content: %w, got: %v", err, upgradePlan) } // normalize name to prevent operator error in upgrade name case sensitivity errors. - ui.Name = strings.ToLower(ui.Name) + upgradePlan.Name = strings.ToLower(upgradePlan.Name) - return ui, err + return upgradePlan, err } diff --git a/x/upgrade/keeper/keeper.go b/x/upgrade/keeper/keeper.go index dc74a1cf981d..dd184602adc0 100644 --- a/x/upgrade/keeper/keeper.go +++ b/x/upgrade/keeper/keeper.go @@ -31,10 +31,6 @@ import ( "github.com/cosmos/cosmos-sdk/types/module" ) -// Deprecated: UpgradeInfoFileName file to store upgrade information -// use x/upgrade/types.UpgradeInfoFilename instead. -const UpgradeInfoFileName string = "upgrade-info.json" - type Keeper struct { homePath string // root directory of app config skipUpgradeHeights map[int64]bool // map of heights to skip for an upgrade