diff --git a/cosmovisor/.gitignore b/cosmovisor/.gitignore new file mode 100644 index 000000000000..fe1602e647d3 --- /dev/null +++ b/cosmovisor/.gitignore @@ -0,0 +1 @@ +/cosmovisor diff --git a/cosmovisor/README.md b/cosmovisor/README.md index e263966a49a0..4e9b7a4b2716 100644 --- a/cosmovisor/README.md +++ b/cosmovisor/README.md @@ -4,6 +4,10 @@ *Note: If new versions of the application are not set up to run in-place store migrations, migrations will need to be run manually before restarting `cosmovisor` with the new binary. For this reason, we recommend applications adopt in-place store migrations.* +## Contributing + +Release branches has the following format `release/cosmovisor/vA.B.x`, where A and B are a number (eg: `release/cosmovisor/v0.1.x`). Releases are tagged using the following format: `cosmovisor/vA.B.C`. + ## Installation To install `cosmovisor`, run the following command: @@ -22,6 +26,8 @@ All arguments passed to `cosmovisor` will be passed to the application binary (a * `DAEMON_NAME` is the name of the binary itself (e.g. `gaiad`, `regend`, `simd`, etc.). * `DAEMON_ALLOW_DOWNLOAD_BINARIES` (*optional*), if set to `true`, will enable auto-downloading of new binaries (for security reasons, this is intended for full nodes rather than validators). By default, `cosmovisor` will not auto-download new binaries. * `DAEMON_RESTART_AFTER_UPGRADE` (*optional*), if set to `true`, will restart the subprocess with the same command-line arguments and flags (but with the new binary) after a successful upgrade. By default, `cosmovisor` stops running after an upgrade and requires the system administrator to manually restart it. Note that `cosmovisor` will not auto-restart the subprocess if there was an error. +* `DAEMON_POLL_INTERVAL` is the interval length in milliseconds for polling the upgrade plan file. Default: 300. +* `UNSAFE_SKIP_BACKUP` (defaults to `false`), if set to `false`, will backup the data before trying the upgrade. Otherwise it will upgrade directly without doing any backup. This is useful (and recommended) in case of failures and when needed to rollback. It is advised to use backup option, i.e., `UNSAFE_SKIP_BACKUP=false` ## Folder Layout @@ -35,8 +41,9 @@ All arguments passed to `cosmovisor` will be passed to the application binary (a │   └── $DAEMON_NAME └── upgrades └── - └── bin - └── $DAEMON_NAME + ├── bin + │   └── $DAEMON_NAME + └── upgrade-info.json ``` The `cosmovisor/` directory incudes a subdirectory for each version of the application (i.e. `genesis` or `upgrades/`). Within each subdirectory is the application binary (i.e. `bin/$DAEMON_NAME`) and any additional auxiliary files associated with each binary. `current` is a symbolic link to the currently active directory (i.e. `genesis` or `upgrades/`). The `name` variable in `upgrades/` is the URI-encoded name of the upgrade as specified in the upgrade module plan. @@ -66,6 +73,18 @@ In order to support downloadable binaries, a tarball for each upgrade binary wil The `DAEMON` specific code and operations (e.g. tendermint config, the application db, syncing blocks, etc.) all work as expected. The application binaries' directives such as command-line flags and environment variables also work as expected. + +### Detecting Upgrades + +`cosmovisor` is polling the `$DAEMON_HOME/data/upgrade-info.json` file for new upgrade instructions. The file is created by the x/upgrade module in `BeginBlocker` when an upgrade is detected and the blockchain reaches the upgrade height. +The following heuristic is applied to detect the upgrade: ++ When starting, `cosmovisor` doesn't know much about currently running upgrade, except the binary which is `current/bin/`. It tries to read the `current/update-info.json` file to get information about the current upgrade name. ++ If neither `cosmovisor/current/upgrade-info.json` nor `data/upgrade-info.json` exist, then `cosmovisor` will wait for `data/upgrade-info.json` file to trigger an upgrade. ++ If `cosmovisor/current/upgrade-info.json` doesn't exist but `data/upgrade-info.json` exists, then `cosmovisor` assumes that whatever is in `data/upgrade-info.json` is a valid upgrade request. In this case `cosmovisor` tries immediately to make an upgrade according to the `name` attribute in `data/upgrade-info.json`. ++ Otherwise, `cosmovisor` waits for changes in `upgrade-info.json`. As soon as a new upgrade name is recorded in the file, `cosmovisor` will trigger an upgrade mechanism. + +When the upgrade mechanism is triggered, `cosmovisor` will start by auto-downloading a new binary (if `DAEMON_ALLOW_DOWNLOAD_BINARIES` is enabled) into `cosmovisor//bin` (where `` is the `upgrade-info.json:name` attribute). `cosmovisor` will then update the `current` symbolic link to point to the new directory and save `data/upgrade-info.json` to `cosmovisor/current/upgrade-info.json`. + ## Auto-Download Generally, `cosmovisor` requires that the system administrator place all relevant binaries on disk before the upgrade happens. However, for people who don't need such control and want an easier setup (maybe they are syncing a non-validating fullnode and want to do little maintenance), there is another option. diff --git a/cosmovisor/args.go b/cosmovisor/args.go index 2b8dbeb5cc7a..e6dcf2c849e3 100644 --- a/cosmovisor/args.go +++ b/cosmovisor/args.go @@ -1,29 +1,39 @@ package cosmovisor import ( - "bufio" + "encoding/json" "errors" "fmt" + "io/ioutil" "net/url" "os" "path/filepath" "strconv" + "time" ) const ( - rootName = "cosmovisor" - genesisDir = "genesis" - upgradesDir = "upgrades" - currentLink = "current" + rootName = "cosmovisor" + genesisDir = "genesis" + upgradesDir = "upgrades" + currentLink = "current" + upgradeFilename = "upgrade-info.json" ) +// 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 - LogBufferSize int + PollInterval time.Duration + UnsafeSkipBackup bool + + // currently running upgrade + currentUpgrade UpgradeInfo } // Root returns the root directory where all info lives @@ -44,10 +54,15 @@ func (cfg *Config) UpgradeBin(upgradeName string) string { // UpgradeDir is the directory named upgrade func (cfg *Config) UpgradeDir(upgradeName string) string { safeName := url.PathEscape(upgradeName) - return filepath.Join(cfg.Root(), upgradesDir, safeName) + return filepath.Join(cfg.Home, rootName, upgradesDir, safeName) +} + +// UpgradeInfoFile is the expected upgrade-info filename created by `x/upgrade/keeper`. +func (cfg *Config) UpgradeInfoFilePath() string { + return filepath.Join(cfg.Home, "data", defaultFilename) } -// Symlink to genesis +// 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) @@ -83,7 +98,8 @@ func (cfg *Config) CurrentBin() (string, error) { } // and return the binary - return filepath.Join(dest, "bin", cfg.Name), nil + binpath := filepath.Join(dest, "bin", cfg.Name) + return binpath, nil } // GetConfigFromEnv will read the environmental variables into a config @@ -102,21 +118,22 @@ func GetConfigFromEnv() (*Config, error) { cfg.RestartAfterUpgrade = true } - logBufferSizeStr := os.Getenv("DAEMON_LOG_BUFFER_SIZE") - if logBufferSizeStr != "" { - logBufferSize, err := strconv.Atoi(logBufferSizeStr) + interval := os.Getenv("DAEMON_POLL_INTERVAL") + if interval != "" { + i, err := strconv.ParseUint(interval, 10, 32) if err != nil { return nil, err } - cfg.LogBufferSize = logBufferSize * 1024 + cfg.PollInterval = time.Millisecond * time.Duration(i) } else { - cfg.LogBufferSize = bufio.MaxScanTokenSize + cfg.PollInterval = 300 * time.Millisecond } + cfg.UnsafeSkipBackup = os.Getenv("UNSAFE_SKIP_BACKUP") == "true" + if err := cfg.validate(); err != nil { return nil, err } - return cfg, nil } @@ -148,3 +165,69 @@ func (cfg *Config) validate() error { return nil } + +// SetCurrentUpgrade sets the named upgrade to be the current link, returns error if this binary doesn't exist +func (cfg *Config) SetCurrentUpgrade(u UpgradeInfo) 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, upgradeFilename)) + 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() UpgradeInfo { + if cfg.currentUpgrade.Name != "" { + return cfg.currentUpgrade + } + + filename := filepath.Join(cfg.Root(), currentLink, upgradeFilename) + _, err := os.Lstat(filename) + var u UpgradeInfo + var bz []byte + if err != nil { // no current directory + goto returnError + } + if bz, err = ioutil.ReadFile(filename); err != nil { + goto returnError + } + if err = json.Unmarshal(bz, &u); err != nil { + goto returnError + } + cfg.currentUpgrade = u + return cfg.currentUpgrade + +returnError: + fmt.Println("[cosmovisor], error reading", filename, err) + cfg.currentUpgrade.Name = "_" + return cfg.currentUpgrade +} diff --git a/cosmovisor/buffer_test.go b/cosmovisor/buffer_test.go new file mode 100644 index 000000000000..04dd2bb4c517 --- /dev/null +++ b/cosmovisor/buffer_test.go @@ -0,0 +1,34 @@ +package cosmovisor_test + +import ( + "bytes" + "sync" +) + +// buffer is a thread safe bytes buffer +type buffer struct { + b bytes.Buffer + m sync.Mutex +} + +func NewBuffer() *buffer { + return &buffer{} +} + +func (b *buffer) Write(bz []byte) (int, error) { + b.m.Lock() + defer b.m.Unlock() + return b.b.Write(bz) +} + +func (b *buffer) String() string { + b.m.Lock() + defer b.m.Unlock() + return b.b.String() +} + +func (b *buffer) Reset() { + b.m.Lock() + defer b.m.Unlock() + b.b.Reset() +} diff --git a/cosmovisor/cmd/cosmovisor/main.go b/cosmovisor/cmd/cosmovisor/main.go index a165acab38f6..f956a2e088f0 100644 --- a/cosmovisor/cmd/cosmovisor/main.go +++ b/cosmovisor/cmd/cosmovisor/main.go @@ -9,7 +9,7 @@ import ( func main() { if err := Run(os.Args[1:]); err != nil { - fmt.Fprintf(os.Stderr, "%+v\n", err) + fmt.Fprintf(os.Stderr, "[cosmovisor] %+v\n", err) os.Exit(1) } } @@ -20,11 +20,19 @@ func Run(args []string) error { if err != nil { return err } + launcher, err := cosmovisor.NewLauncher(cfg) + if err != nil { + return err + } - doUpgrade, err := cosmovisor.LaunchProcess(cfg, args, os.Stdout, os.Stderr) + doUpgrade, err := launcher.Run(args, os.Stdout, os.Stderr) // if RestartAfterUpgrade, we launch after a successful upgrade (only condition LaunchProcess returns nil) for cfg.RestartAfterUpgrade && err == nil && doUpgrade { - doUpgrade, err = cosmovisor.LaunchProcess(cfg, args, os.Stdout, os.Stderr) + fmt.Println("[cosmovisor] upgrade detected, relaunching the app ", cfg.Name) + doUpgrade, err = launcher.Run(args, os.Stdout, os.Stderr) + } + if doUpgrade && err == nil { + fmt.Println("[cosmovisor] upgrade detected, DAEMON_RESTART_AFTER_UPGRADE is off. Verify new upgrade and start cosmovisor again.") } return err } diff --git a/cosmovisor/go.mod b/cosmovisor/go.mod index fe6be0cdd4e0..70d184cba36e 100644 --- a/cosmovisor/go.mod +++ b/cosmovisor/go.mod @@ -1,9 +1,9 @@ module github.com/cosmos/cosmos-sdk/cosmovisor -go 1.14 +go 1.15 require ( github.com/hashicorp/go-getter v1.4.1 - github.com/otiai10/copy v1.2.0 - github.com/stretchr/testify v1.6.1 + github.com/otiai10/copy v1.4.2 + github.com/stretchr/testify v1.7.0 ) diff --git a/cosmovisor/go.sum b/cosmovisor/go.sum index d4e8919a2680..5d9538f4f96b 100644 --- a/cosmovisor/go.sum +++ b/cosmovisor/go.sum @@ -61,20 +61,20 @@ github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnG github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/otiai10/copy v1.2.0 h1:HvG945u96iNadPoG2/Ja2+AUJeW5YuFQMixq9yirC+k= -github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= +github.com/otiai10/copy v1.4.2 h1:RTiz2sol3eoXPLF4o+YWqEybwfUa/Q2Nkc4ZIUs3fwI= +github.com/otiai10/copy v1.4.2/go.mod h1:XWfuS3CrI0R6IE0FbgHsEazaXO8G0LpMp9o8tos0x4E= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v1.0.0 h1:TJIWdbX0B+kpNagQrjgq8bCMrbhiuX73M2XwgtDMoOI= github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= -github.com/otiai10/mint v1.3.1 h1:BCmzIS3n71sGfHB5NMNDB3lHYPz8fWSkCAErHed//qc= -github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/otiai10/mint v1.3.2 h1:VYWnrP5fXmz1MXvjuUvcBrXSjGE6xjON+axB/UrpO3E= +github.com/otiai10/mint v1.3.2/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/ulikunitz/xz v0.5.5 h1:pFrO0lVpTBXLpYw+pnLj6TbvHuyjXMfjGeCwSqCVwok= github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= diff --git a/cosmovisor/process.go b/cosmovisor/process.go index 6a67f65e162e..98e0b9ef5ee6 100644 --- a/cosmovisor/process.go +++ b/cosmovisor/process.go @@ -1,57 +1,50 @@ package cosmovisor import ( - "bufio" + "encoding/json" "fmt" "io" + "io/ioutil" "log" "os" "os/exec" "os/signal" + "path/filepath" "strings" - "sync" "syscall" + "time" + + "github.com/otiai10/copy" ) -// LaunchProcess runs a subprocess and returns when the subprocess exits, -// either when it dies, or *after* a successful upgrade. -func LaunchProcess(cfg *Config, args []string, stdout, stderr io.Writer) (bool, error) { - bin, err := cfg.CurrentBin() +type Launcher struct { + cfg *Config + fw *fileWatcher +} + +func NewLauncher(cfg *Config) (Launcher, error) { + fw, err := newUpgradeFileWatcher(cfg.UpgradeInfoFilePath(), cfg.PollInterval) + return Launcher{cfg, fw}, err +} + +// Run launches the app in a subprocess and returns when the subprocess (app) +// exits (either when it dies, or *after* a successful upgrade.) and upgrade finished. +// Returns true if the upgrade request was detected and the upgrade process started. +func (l Launcher) Run(args []string, stdout, stderr io.Writer) (bool, error) { + bin, err := l.cfg.CurrentBin() if err != nil { return false, fmt.Errorf("error creating symlink to genesis: %w", err) } if err := EnsureBinary(bin); err != nil { - return false, fmt.Errorf("current binary invalid: %w", err) + return false, fmt.Errorf("current binary is invalid: %w", err) } - + fmt.Println("[cosmovisor] running ", bin, args) cmd := exec.Command(bin, args...) - outpipe, err := cmd.StdoutPipe() - if err != nil { - return false, err - } - - errpipe, err := cmd.StderrPipe() - if err != nil { - return false, err - } - - scanOut := bufio.NewScanner(io.TeeReader(outpipe, stdout)) - scanErr := bufio.NewScanner(io.TeeReader(errpipe, stderr)) - // set scanner's buffer size to cfg.LogBufferSize, and ensure larger than bufio.MaxScanTokenSize otherwise fallback to bufio.MaxScanTokenSize - var maxCapacity int - if cfg.LogBufferSize < bufio.MaxScanTokenSize { - maxCapacity = bufio.MaxScanTokenSize - } else { - maxCapacity = cfg.LogBufferSize - } - bufOut := make([]byte, maxCapacity) - bufErr := make([]byte, maxCapacity) - scanOut.Buffer(bufOut, maxCapacity) - scanErr.Buffer(bufErr, maxCapacity) - + cmd.Stdout = stdout + cmd.Stderr = stderr if err := cmd.Start(); err != nil { - return false, fmt.Errorf("launching process %s %s: %w", bin, strings.Join(args, " "), err) + return false, fmt.Errorf("launching process %s %s failed: %w", bin, strings.Join(args, " "), err) } sigs := make(chan os.Signal, 1) @@ -59,95 +52,93 @@ func LaunchProcess(cfg *Config, args []string, stdout, stderr io.Writer) (bool, go func() { sig := <-sigs if err := cmd.Process.Signal(sig); err != nil { - log.Fatal(err) + log.Fatal(bin, "terminated. Error:", err) } }() - // three ways to exit - command ends, find regexp in scanOut, find regexp in scanErr - upgradeInfo, err := WaitForUpgradeOrExit(cmd, scanOut, scanErr) - if err != nil { + needsUpdate, err := l.WaitForUpgradeOrExit(cmd) + if err != nil || !needsUpdate { return false, err } - - if upgradeInfo != nil { - return true, DoUpgrade(cfg, upgradeInfo) + if err := doBackup(l.cfg); err != nil { + return false, err } - return false, nil -} - -// WaitResult is used to wrap feedback on cmd state with some mutex logic. -// This is needed as multiple go-routines can affect this - two read pipes that can trigger upgrade -// As well as the command, which can fail -type WaitResult struct { - // both err and info may be updated from several go-routines - // access is wrapped by mutex and should only be done through methods - err error - info *UpgradeInfo - mutex sync.Mutex -} - -// AsResult reads the data protected by mutex to avoid race conditions -func (u *WaitResult) AsResult() (*UpgradeInfo, error) { - u.mutex.Lock() - defer u.mutex.Unlock() - return u.info, u.err + return true, DoUpgrade(l.cfg, l.fw.currentInfo) } -// SetError will set with the first error using a mutex -// don't set it once info is set, that means we chose to kill the process -func (u *WaitResult) SetError(myErr error) { - u.mutex.Lock() - defer u.mutex.Unlock() - if u.info == nil && myErr != nil { - u.err = myErr - } -} +// WaitForUpgradeOrExit checks upgrade plan file created by the app. +// 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, 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 ...` +func (l Launcher) WaitForUpgradeOrExit(cmd *exec.Cmd) (bool, error) { + currentUpgrade := l.cfg.UpgradeInfo() + var cmdDone = make(chan error) + go func() { + cmdDone <- cmd.Wait() + }() -// SetUpgrade sets first non-nil upgrade info, ensure error is then nil -// pass in a command to shutdown on successful upgrade -func (u *WaitResult) SetUpgrade(up *UpgradeInfo) { - u.mutex.Lock() - defer u.mutex.Unlock() - if u.info == nil && up != nil { - u.info = up - u.err = nil + select { + case <-l.fw.MonitorUpdate(currentUpgrade): + // upgrade - kill the process and restart + _ = cmd.Process.Kill() + case err := <-cmdDone: + l.fw.Stop() + // no error -> command exits normally (eg. short command like `gaiad version`) + if err == nil { + return false, nil + } + // the app x/upgrade causes a panic and the app can die before the filwatcher finds the + // update, so we need to recheck update-info file. + if !l.fw.CheckUpdate(currentUpgrade) { + return false, err + } } + return true, nil } -// WaitForUpgradeOrExit listens to both output streams of the process, as well as the process state itself -// When it returns, the process is finished and all streams have closed. -// -// It returns (info, nil) if an upgrade should be initiated (and we killed the process) -// It returns (nil, err) if the process died by itself, or there was an issue reading the pipes -// It returns (nil, 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 ...` -func WaitForUpgradeOrExit(cmd *exec.Cmd, scanOut, scanErr *bufio.Scanner) (*UpgradeInfo, error) { - var res WaitResult +func doBackup(cfg *Config) error { + // take backup if `UNSAFE_SKIP_BACKUP` is not set. + if !cfg.UnsafeSkipBackup { + // check if upgrade-info.json is not empty. + var uInfo UpgradeInfo + upgradeInfoFile, err := ioutil.ReadFile(filepath.Join(cfg.Home, "data", "upgrade-info.json")) + if err != nil { + return fmt.Errorf("error while reading upgrade-info.json: %w", err) + } - waitScan := func(scan *bufio.Scanner) { - upgrade, err := WaitForUpdate(scan) + err = json.Unmarshal(upgradeInfoFile, &uInfo) if err != nil { - res.SetError(err) - } else if upgrade != nil { - res.SetUpgrade(upgrade) - // now we need to kill the process - _ = cmd.Process.Kill() + return err } - } - // wait for the scanners, which can trigger upgrade and kill cmd - go waitScan(scanOut) - go waitScan(scanErr) + if uInfo.Name == "" { + return fmt.Errorf("upgrade-info.json is empty") + } + + // a destination directory, Format YYYY-MM-DD + st := time.Now() + stStr := fmt.Sprintf("%d-%d-%d", st.Year(), st.Month(), st.Day()) + dst := filepath.Join(cfg.Home, fmt.Sprintf("data"+"-backup-%s", stStr)) + + fmt.Printf("starting to take backup of data directory at time %s", st) - // if the command exits normally (eg. short command like `gaiad version`), just return (nil, nil) - // we often get broken read pipes if it runs too fast. - // if we had upgrade info, we would have killed it, and thus got a non-nil error code - err := cmd.Wait() - if err == nil { - return nil, nil + // copy the $DAEMON_HOME/data to a backup dir + err = copy.Copy(filepath.Join(cfg.Home, "data"), dst) + + if err != nil { + return fmt.Errorf("error while taking data backup: %w", err) + } + + // backup is done, lets check endtime to calculate total time taken for backup process + et := time.Now() + timeTaken := et.Sub(st) + fmt.Printf("backup saved at location: %s, completed at time: %s\n"+ + "time taken to complete the backup: %s", dst, et, timeTaken) } - // this will set the error code if it wasn't killed due to upgrade - res.SetError(err) - return res.AsResult() + + return nil } diff --git a/cosmovisor/process_test.go b/cosmovisor/process_test.go index 3b9a553ed638..407954a308d4 100644 --- a/cosmovisor/process_test.go +++ b/cosmovisor/process_test.go @@ -4,7 +4,7 @@ package cosmovisor_test import ( - "bytes" + "fmt" "testing" "github.com/stretchr/testify/suite" @@ -23,93 +23,108 @@ func TestProcessTestSuite(t *testing.T) { // TestLaunchProcess will try running the script a few times and watch upgrades work properly // and args are passed through func (s *processTestSuite) TestLaunchProcess() { + // binaries from testdata/validate directory + require := s.Require() home := copyTestData(s.T(), "validate") - cfg := &cosmovisor.Config{Home: home, Name: "dummyd"} + cfg := &cosmovisor.Config{Home: home, Name: "dummyd", PollInterval: 20, UnsafeSkipBackup: true} // should run the genesis binary and produce expected output - var stdout, stderr bytes.Buffer + var stdout, stderr = NewBuffer(), NewBuffer() currentBin, err := cfg.CurrentBin() - s.Require().NoError(err) + require.NoError(err) + require.Equal(cfg.GenesisBin(), currentBin) - s.Require().Equal(cfg.GenesisBin(), currentBin) + launcher, err := cosmovisor.NewLauncher(cfg) + require.NoError(err) - args := []string{"foo", "bar", "1234"} - doUpgrade, err := cosmovisor.LaunchProcess(cfg, args, &stdout, &stderr) - s.Require().NoError(err) - s.Require().True(doUpgrade) - s.Require().Equal("", stderr.String()) - s.Require().Equal("Genesis foo bar 1234\nUPGRADE \"chain2\" NEEDED at height: 49: {}\n", stdout.String()) + upgradeFile := cfg.UpgradeInfoFilePath() + args := []string{"foo", "bar", "1234", upgradeFile} + doUpgrade, err := launcher.Run(args, stdout, stderr) + require.NoError(err) + require.True(doUpgrade) + require.Equal("", stderr.String()) + require.Equal(fmt.Sprintf("Genesis foo bar 1234 %s\nUPGRADE \"chain2\" NEEDED at height: 49: {}\n", upgradeFile), + stdout.String()) // ensure this is upgraded now and produces new output currentBin, err = cfg.CurrentBin() - s.Require().NoError(err) - s.Require().Equal(cfg.UpgradeBin("chain2"), currentBin) + require.NoError(err) + + require.Equal(cfg.UpgradeBin("chain2"), currentBin) args = []string{"second", "run", "--verbose"} stdout.Reset() stderr.Reset() - doUpgrade, err = cosmovisor.LaunchProcess(cfg, args, &stdout, &stderr) - s.Require().NoError(err) - s.Require().False(doUpgrade) - s.Require().Equal("", stderr.String()) - s.Require().Equal("Chain 2 is live!\nArgs: second run --verbose\nFinished successfully\n", stdout.String()) + + doUpgrade, err = launcher.Run(args, stdout, stderr) + require.NoError(err) + require.False(doUpgrade) + require.Equal("", stderr.String()) + require.Equal("Chain 2 is live!\nArgs: second run --verbose\nFinished successfully\n", stdout.String()) // ended without other upgrade - s.Require().Equal(cfg.UpgradeBin("chain2"), currentBin) + require.Equal(cfg.UpgradeBin("chain2"), currentBin) } // TestLaunchProcess will try running the script a few times and watch upgrades work properly // and args are passed through func (s *processTestSuite) TestLaunchProcessWithDownloads() { - // this is a fun path - // genesis -> "chain2" = zip_binary - // zip_binary -> "chain3" = ref_zipped -> zip_directory - // zip_directory no upgrade + // test case upgrade path (binaries from testdata/download directory): + // genesis -> chain2-zip_bin + // chain2-zip_bin -> ref_to_chain3-zip_dir.json = (json for the next download instructions) -> chain3-zip_dir + // chain3-zip_dir - doesn't upgrade + require := s.Require() home := copyTestData(s.T(), "download") - cfg := &cosmovisor.Config{Home: home, Name: "autod", AllowDownloadBinaries: true} + cfg := &cosmovisor.Config{Home: home, Name: "autod", AllowDownloadBinaries: true, PollInterval: 100, UnsafeSkipBackup: true} + upgradeFilename := cfg.UpgradeInfoFilePath() // should run the genesis binary and produce expected output - var stdout, stderr bytes.Buffer currentBin, err := cfg.CurrentBin() - s.Require().NoError(err) + require.NoError(err) + require.Equal(cfg.GenesisBin(), currentBin) - s.Require().Equal(cfg.GenesisBin(), currentBin) - args := []string{"some", "args"} - doUpgrade, err := cosmovisor.LaunchProcess(cfg, args, &stdout, &stderr) - s.Require().NoError(err) - s.Require().True(doUpgrade) - s.Require().Equal("", stderr.String()) - s.Require().Equal("Preparing auto-download some args\n"+`ERROR: UPGRADE "chain2" NEEDED at height: 49: {"binaries":{"linux/amd64":"https://github.com/cosmos/cosmos-sdk/raw/51249cb93130810033408934454841c98423ed4b/cosmovisor/testdata/repo/zip_binary/autod.zip?checksum=sha256:dc48829b4126ae95bc0db316c66d4e9da5f3db95e212665b6080638cca77e998"}} module=main`+"\n", stdout.String()) + launcher, err := cosmovisor.NewLauncher(cfg) + require.NoError(err) - // ensure this is upgraded now and produces new output + var 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()) + require.Equal("Genesis autod. Args: some args "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain2" NEEDED at height: 49: zip_binary`+"\n", stdout.String()) currentBin, err = cfg.CurrentBin() - s.Require().NoError(err) - s.Require().Equal(cfg.UpgradeBin("chain2"), currentBin) - args = []string{"run", "--fast"} + require.NoError(err) + require.Equal(cfg.UpgradeBin("chain2"), currentBin) + + // start chain2 stdout.Reset() stderr.Reset() - doUpgrade, err = cosmovisor.LaunchProcess(cfg, args, &stdout, &stderr) - s.Require().NoError(err) - s.Require().True(doUpgrade) - s.Require().Equal("", stderr.String()) - s.Require().Equal("Chain 2 from zipped binary link to referral\nArgs: run --fast\n"+`ERROR: UPGRADE "chain3" NEEDED at height: 936: https://github.com/cosmos/cosmos-sdk/raw/0eae1a50612b8bf803336d35055896fbddaa1ddd/cosmovisor/testdata/repo/ref_zipped?checksum=sha256:0a428575de718ed3cf0771c9687eefaf6f19359977eca4d94a0abd0e11ef8e64 module=main`+"\n", stdout.String()) + args = []string{"run", "--fast", upgradeFilename} + doUpgrade, err = launcher.Run(args, stdout, stderr) + require.NoError(err) + require.Equal("", stderr.String()) + require.Equal("Chain 2 from zipped binary\nArgs: run --fast "+upgradeFilename+"\n"+`ERROR: UPGRADE "chain3" NEEDED at height: 936: ref_to_chain3-zip_dir.json module=main`+"\n", stdout.String()) // ended with one more upgrade + require.True(doUpgrade) currentBin, err = cfg.CurrentBin() - s.Require().NoError(err) - s.Require().Equal(cfg.UpgradeBin("chain3"), currentBin) - // make sure this is the proper binary now.... - args = []string{"end", "--halt"} + require.NoError(err) + require.Equal(cfg.UpgradeBin("chain3"), currentBin) + + // run the last chain + args = []string{"end", "--halt", upgradeFilename} stdout.Reset() stderr.Reset() - doUpgrade, err = cosmovisor.LaunchProcess(cfg, args, &stdout, &stderr) - s.Require().NoError(err) - s.Require().False(doUpgrade) - s.Require().Equal("", stderr.String()) - s.Require().Equal("Chain 2 from zipped directory\nArgs: end --halt\n", stdout.String()) + doUpgrade, err = launcher.Run(args, stdout, stderr) + require.NoError(err) + require.False(doUpgrade) + require.Equal("", stderr.String()) + require.Equal("Chain 3 from zipped directory\nArgs: end --halt "+upgradeFilename+"\n", stdout.String()) // and this doesn't upgrade currentBin, err = cfg.CurrentBin() - s.Require().NoError(err) - s.Require().Equal(cfg.UpgradeBin("chain3"), currentBin) + require.NoError(err) + require.Equal(cfg.UpgradeBin("chain3"), currentBin) } diff --git a/cosmovisor/scanner_test.go b/cosmovisor/scanner_test.go index 9e42410c7be4..cabaf8304479 100644 --- a/cosmovisor/scanner_test.go +++ b/cosmovisor/scanner_test.go @@ -1,63 +1,62 @@ -package cosmovisor_test +package cosmovisor import ( - "bufio" - "io" + "path/filepath" "testing" - "github.com/cosmos/cosmos-sdk/cosmovisor" - "github.com/stretchr/testify/require" ) -func TestWaitForInfo(t *testing.T) { - cases := map[string]struct { - write []string - expectUpgrade *cosmovisor.UpgradeInfo +func TestParseUpgradeInfoFile(t *testing.T) { + cases := []struct { + filename string + expectUpgrade UpgradeInfo expectErr bool - }{ - "no match": { - write: []string{"some", "random\ninfo\n"}, - }, - "match name with no info": { - write: []string{"first line\n", `UPGRADE "myname" NEEDED at height: 123: `, "\nnext line\n"}, - expectUpgrade: &cosmovisor.UpgradeInfo{ - Name: "myname", - Info: "", - }, - }, - "match name with info": { - write: []string{"first line\n", `UPGRADE "take2" NEEDED at height: 123: DownloadData here!`, "\nnext line\n"}, - expectUpgrade: &cosmovisor.UpgradeInfo{ - Name: "take2", - Info: "DownloadData", - }, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - r, w := io.Pipe() - scan := bufio.NewScanner(r) - - // write all info in separate routine - go func() { - for _, line := range tc.write { - n, err := w.Write([]byte(line)) - require.NoError(t, err) - require.Equal(t, len(line), n) - } - w.Close() - }() + }{{ + filename: "f1-good.json", + expectUpgrade: UpgradeInfo{Name: "upgrade1", Info: "some info", Height: 123}, + expectErr: false, + }, { + filename: "f2-bad-type.json", + expectUpgrade: UpgradeInfo{}, + expectErr: true, + }, { + filename: "f2-bad-type-2.json", + expectUpgrade: UpgradeInfo{}, + expectErr: true, + }, { + filename: "f3-empty.json", + expectUpgrade: UpgradeInfo{}, + expectErr: true, + }, { + filename: "f4-empty-obj.json", + expectUpgrade: UpgradeInfo{}, + expectErr: true, + }, { + filename: "f5-partial-obj-1.json", + expectUpgrade: UpgradeInfo{}, + expectErr: true, + }, { + filename: "f5-partial-obj-2.json", + expectUpgrade: UpgradeInfo{}, + expectErr: true, + }, { + filename: "unknown.json", + expectUpgrade: UpgradeInfo{}, + expectErr: true, + }} - // now scan the info - info, err := cosmovisor.WaitForUpdate(scan) + for i := range cases { + tc := cases[i] + t.Run(tc.filename, func(t *testing.T) { + require := require.New(t) + ui, err := parseUpgradeInfoFile(filepath.Join(".", "testdata", "upgrade-files", tc.filename)) if tc.expectErr { - require.Error(t, err) - return + require.Error(err) + } else { + require.NoError(err) + require.Equal(tc.expectUpgrade, ui) } - require.NoError(t, err) - require.Equal(t, tc.expectUpgrade, info) }) } } diff --git a/cosmovisor/testdata/download/cosmovisor/genesis/bin/autod b/cosmovisor/testdata/download/cosmovisor/genesis/bin/autod index 113cce7f797f..816b3c3b634f 100755 --- a/cosmovisor/testdata/download/cosmovisor/genesis/bin/autod +++ b/cosmovisor/testdata/download/cosmovisor/genesis/bin/autod @@ -1,7 +1,12 @@ #!/bin/sh -echo Preparing auto-download $@ -sleep 1 -echo 'ERROR: UPGRADE "chain2" NEEDED at height: 49: {"binaries":{"linux/amd64":"https://github.com/cosmos/cosmos-sdk/raw/51249cb93130810033408934454841c98423ed4b/cosmovisor/testdata/repo/zip_binary/autod.zip?checksum=sha256:dc48829b4126ae95bc0db316c66d4e9da5f3db95e212665b6080638cca77e998"}} module=main' -sleep 4 +echo Genesis autod. Args: $@ +sleep 0.1 +echo 'ERROR: UPGRADE "chain2" NEEDED at height: 49: zip_binary' + +# create upgrade info +# this info contains directly information about binaries (in chain2->chain3 update we test with info containing a link to the file with an address for the new chain binary) +echo '{"name":"chain2","height":49,"info":"{\"binaries\":{\"linux/amd64\":\"https://github.com/cosmos/cosmos-sdk/raw/robert/cosmvisor-file-watch/cosmovisor/testdata/repo/chain2-zip_bin/autod.zip?checksum=sha256:960cad22d1684e4baf3e8781334c28e16d0e329497762aaa3b5c11e2184086bb\"}}"}' > $3 + +sleep 0.1 echo Never should be printed!!! diff --git a/cosmovisor/testdata/download/data/.gitkeep b/cosmovisor/testdata/download/data/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/cosmovisor/testdata/repo/chain2-zip_bin/autod b/cosmovisor/testdata/repo/chain2-zip_bin/autod new file mode 100755 index 000000000000..eefc6c495d3b --- /dev/null +++ b/cosmovisor/testdata/repo/chain2-zip_bin/autod @@ -0,0 +1,13 @@ +#!/bin/sh + +echo Chain 2 from zipped binary +echo Args: $@ +# note that we just have a url (follow the ref), not a full link +echo 'ERROR: UPGRADE "chain3" NEEDED at height: 936: ref_to_chain3-zip_dir.json module=main' + +# this update info doesn't contain binaries, instead it is a reference for further download instructions. +# echo '{"name":"chain3","height":936,"info":"{\"binaries\":{\"linux/amd64\":\"https://github.com/cosmos/cosmos-sdk/raw/robert/cosmvisor-file-watch/cosmovisor/testdata/repo/ref_to_chain3-zip_dir.json\"}}"}' > $3 +echo '{"name":"chain3","height":936,"info":"https://github.com/cosmos/cosmos-sdk/raw/robert/cosmvisor-file-watch/cosmovisor/testdata/repo/ref_to_chain3-zip_dir.json"}' > $3 + +sleep 1 +echo 'Do not print' diff --git a/cosmovisor/testdata/repo/chain2-zip_bin/autod.zip b/cosmovisor/testdata/repo/chain2-zip_bin/autod.zip new file mode 100644 index 000000000000..4364f996c869 Binary files /dev/null and b/cosmovisor/testdata/repo/chain2-zip_bin/autod.zip differ diff --git a/cosmovisor/testdata/repo/chain3-zip_dir/autod.zip b/cosmovisor/testdata/repo/chain3-zip_dir/autod.zip new file mode 100644 index 000000000000..dffb1aba4668 Binary files /dev/null and b/cosmovisor/testdata/repo/chain3-zip_dir/autod.zip differ diff --git a/cosmovisor/testdata/repo/chain3-zip_dir/bin/autod b/cosmovisor/testdata/repo/chain3-zip_dir/bin/autod new file mode 100755 index 000000000000..235c82b6300d --- /dev/null +++ b/cosmovisor/testdata/repo/chain3-zip_dir/bin/autod @@ -0,0 +1,4 @@ +#!/bin/sh + +echo Chain 3 from zipped directory +echo Args: $@ diff --git a/cosmovisor/testdata/repo/ref_to_chain3-zip_dir.json b/cosmovisor/testdata/repo/ref_to_chain3-zip_dir.json new file mode 100644 index 000000000000..4042e4ecde72 --- /dev/null +++ b/cosmovisor/testdata/repo/ref_to_chain3-zip_dir.json @@ -0,0 +1,5 @@ +{ + "binaries": { + "linux/amd64": "https://github.com/cosmos/cosmos-sdk/raw/robert/cosmvisor-file-watch/cosmovisor/testdata/repo/chain3-zip_dir/autod.zip?checksum=sha256:8951f52a0aea8617de0ae459a20daf704c29d259c425e60d520e363df0f166b4" + } +} diff --git a/cosmovisor/testdata/repo/ref_zipped b/cosmovisor/testdata/repo/ref_zipped deleted file mode 100644 index fd63c7161f81..000000000000 --- a/cosmovisor/testdata/repo/ref_zipped +++ /dev/null @@ -1,5 +0,0 @@ -{ - "binaries": { - "linux/amd64": "https://github.com/cosmos/cosmos-sdk/raw/aa5d6140ad4011bb33d472dca8246a0dcbe223ee/cosmovisor/testdata/repo/zip_directory/autod.zip?checksum=sha256:3784e4574cad69b67e34d4ea4425eff140063a3870270a301d6bb24a098a27ae" - } -} \ No newline at end of file diff --git a/cosmovisor/testdata/repo/zip_binary/autod b/cosmovisor/testdata/repo/zip_binary/autod deleted file mode 100755 index 4ed1dea36de4..000000000000 --- a/cosmovisor/testdata/repo/zip_binary/autod +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -echo Chain 2 from zipped binary link to referral -echo Args: $@ -# note that we just have a url (follow the ref), not a full link -echo 'ERROR: UPGRADE "chain3" NEEDED at height: 936: https://github.com/cosmos/cosmos-sdk/raw/0eae1a50612b8bf803336d35055896fbddaa1ddd/cosmovisor/testdata/repo/ref_zipped?checksum=sha256:0a428575de718ed3cf0771c9687eefaf6f19359977eca4d94a0abd0e11ef8e64 module=main' -sleep 4 -echo 'Do not print' diff --git a/cosmovisor/testdata/repo/zip_binary/autod.zip b/cosmovisor/testdata/repo/zip_binary/autod.zip deleted file mode 100644 index 0fe45f18f625..000000000000 Binary files a/cosmovisor/testdata/repo/zip_binary/autod.zip and /dev/null differ diff --git a/cosmovisor/testdata/repo/zip_directory/autod.zip b/cosmovisor/testdata/repo/zip_directory/autod.zip deleted file mode 100644 index 225cd4672a78..000000000000 Binary files a/cosmovisor/testdata/repo/zip_directory/autod.zip and /dev/null differ diff --git a/cosmovisor/testdata/repo/zip_directory/bin/autod b/cosmovisor/testdata/repo/zip_directory/bin/autod deleted file mode 100755 index ea3a6f8311a4..000000000000 --- a/cosmovisor/testdata/repo/zip_directory/bin/autod +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh - -echo Chain 2 from zipped directory -echo Args: $@ diff --git a/cosmovisor/testdata/upgrade-files/f1-good.json b/cosmovisor/testdata/upgrade-files/f1-good.json new file mode 100644 index 000000000000..fd64644e184c --- /dev/null +++ b/cosmovisor/testdata/upgrade-files/f1-good.json @@ -0,0 +1 @@ +{"name": "upgrade1", "info": "some info", "height": 123} diff --git a/cosmovisor/testdata/upgrade-files/f2-bad-type-2.json b/cosmovisor/testdata/upgrade-files/f2-bad-type-2.json new file mode 100644 index 000000000000..8019e2a0c146 --- /dev/null +++ b/cosmovisor/testdata/upgrade-files/f2-bad-type-2.json @@ -0,0 +1 @@ +{"name": "upgrade1", "heigh": "123"} diff --git a/cosmovisor/testdata/upgrade-files/f2-bad-type.json b/cosmovisor/testdata/upgrade-files/f2-bad-type.json new file mode 100644 index 000000000000..4abd0f77c789 --- /dev/null +++ b/cosmovisor/testdata/upgrade-files/f2-bad-type.json @@ -0,0 +1 @@ +{"name": "upgrade1", "info": 123, "heigh": 123} diff --git a/cosmovisor/testdata/upgrade-files/f3-empty.json b/cosmovisor/testdata/upgrade-files/f3-empty.json new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/cosmovisor/testdata/upgrade-files/f3-empty.json @@ -0,0 +1 @@ + diff --git a/cosmovisor/testdata/upgrade-files/f4-empty-obj.json b/cosmovisor/testdata/upgrade-files/f4-empty-obj.json new file mode 100644 index 000000000000..0967ef424bce --- /dev/null +++ b/cosmovisor/testdata/upgrade-files/f4-empty-obj.json @@ -0,0 +1 @@ +{} diff --git a/cosmovisor/testdata/upgrade-files/f5-partial-obj-1.json b/cosmovisor/testdata/upgrade-files/f5-partial-obj-1.json new file mode 100644 index 000000000000..19aecd9bcdce --- /dev/null +++ b/cosmovisor/testdata/upgrade-files/f5-partial-obj-1.json @@ -0,0 +1 @@ +{"name": "upgrade2"} diff --git a/cosmovisor/testdata/upgrade-files/f5-partial-obj-2.json b/cosmovisor/testdata/upgrade-files/f5-partial-obj-2.json new file mode 100644 index 000000000000..0f13ee91be23 --- /dev/null +++ b/cosmovisor/testdata/upgrade-files/f5-partial-obj-2.json @@ -0,0 +1 @@ +{"height": 1} diff --git a/cosmovisor/testdata/validate/cosmovisor/genesis/bin/dummyd b/cosmovisor/testdata/validate/cosmovisor/genesis/bin/dummyd index c240b802a0be..8d54d60d076e 100755 --- a/cosmovisor/testdata/validate/cosmovisor/genesis/bin/dummyd +++ b/cosmovisor/testdata/validate/cosmovisor/genesis/bin/dummyd @@ -2,6 +2,8 @@ echo Genesis $@ sleep 1 +test -z $4 && exit 1001 echo 'UPGRADE "chain2" NEEDED at height: 49: {}' +echo '{"name":"chain2","height":49,"info":""}' > $4 sleep 2 echo Never should be printed!!! diff --git a/cosmovisor/testdata/validate/data/.gitkeep b/cosmovisor/testdata/validate/data/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/cosmovisor/upgrade.go b/cosmovisor/upgrade.go index beba31c6536f..2495d3096909 100644 --- a/cosmovisor/upgrade.go +++ b/cosmovisor/upgrade.go @@ -17,12 +17,12 @@ import ( // DoUpgrade will be called after the log message has been parsed and the process has terminated. // We can now make any changes to the underlying directory without interference and leave it // in a state, so we can make a proper restart -func DoUpgrade(cfg *Config, info *UpgradeInfo) error { +func DoUpgrade(cfg *Config, info UpgradeInfo) error { // Simplest case is to switch the link err := EnsureBinary(cfg.UpgradeBin(info.Name)) if err == nil { // we have the binary - do it - return cfg.SetCurrentUpgrade(info.Name) + return cfg.SetCurrentUpgrade(info) } // if auto-download is disabled, we fail if !cfg.AllowDownloadBinaries { @@ -36,7 +36,7 @@ func DoUpgrade(cfg *Config, info *UpgradeInfo) error { // If not there, then we try to download it... maybe if err := DownloadBinary(cfg, info); err != nil { - return fmt.Errorf("cannot download binary: %w", err) + return fmt.Errorf("cannot download binary. %w", err) } // and then set the binary again @@ -44,11 +44,11 @@ func DoUpgrade(cfg *Config, info *UpgradeInfo) error { return fmt.Errorf("downloaded binary doesn't check out: %w", err) } - return cfg.SetCurrentUpgrade(info.Name) + return cfg.SetCurrentUpgrade(info) } // DownloadBinary will grab the binary and place it in the proper directory -func DownloadBinary(cfg *Config, info *UpgradeInfo) error { +func DownloadBinary(cfg *Config, info UpgradeInfo) error { url, err := GetDownloadURL(info) if err != nil { return err @@ -101,7 +101,7 @@ type UpgradeConfig struct { } // GetDownloadURL will check if there is an arch-dependent binary specified in Info -func GetDownloadURL(info *UpgradeInfo) (string, error) { +func GetDownloadURL(info UpgradeInfo) (string, error) { doc := strings.TrimSpace(info.Info) // if this is a url, then we download that and try to get a new doc with the real info if _, err := url.Parse(doc); err == nil { @@ -146,33 +146,6 @@ func OSArch() string { return fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) } -// SetCurrentUpgrade sets the named upgrade to be the current link, returns error if this binary doesn't exist -func (cfg *Config) SetCurrentUpgrade(upgradeName string) error { - // ensure named upgrade exists - bin := cfg.UpgradeBin(upgradeName) - - if err := EnsureBinary(bin); err != nil { - return err - } - - // set a symbolic link - link := filepath.Join(cfg.Root(), currentLink) - safeName := url.PathEscape(upgradeName) - 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) - } - - return nil -} - // EnsureBinary ensures the file exists and is executable, or returns an error func EnsureBinary(path string) error { info, err := os.Stat(path) diff --git a/cosmovisor/upgrade_test.go b/cosmovisor/upgrade_test.go index 371db1ef3c93..d8f881194275 100644 --- a/cosmovisor/upgrade_test.go +++ b/cosmovisor/upgrade_test.go @@ -37,7 +37,7 @@ func (s *upgradeTestSuite) TestCurrentBin() { // ensure we cannot set this to an invalid value for _, name := range []string{"missing", "nobin", "noexec"} { - s.Require().Error(cfg.SetCurrentUpgrade(name), name) + s.Require().Error(cfg.SetCurrentUpgrade(cosmovisor.UpgradeInfo{Name: name}), name) currentBin, err := cfg.CurrentBin() s.Require().NoError(err) @@ -46,15 +46,15 @@ func (s *upgradeTestSuite) TestCurrentBin() { } // try a few times to make sure this can be reproduced - for _, upgrade := range []string{"chain2", "chain3", "chain2"} { + for _, name := range []string{"chain2", "chain3", "chain2"} { // now set it to a valid upgrade and make sure CurrentBin is now set properly - err = cfg.SetCurrentUpgrade(upgrade) + err = cfg.SetCurrentUpgrade(cosmovisor.UpgradeInfo{Name: name}) s.Require().NoError(err) // we should see current point to the new upgrade dir currentBin, err := cfg.CurrentBin() s.Require().NoError(err) - s.Require().Equal(cfg.UpgradeBin(upgrade), currentBin) + s.Require().Equal(cfg.UpgradeBin(name), currentBin) } } @@ -67,7 +67,7 @@ func (s *upgradeTestSuite) TestCurrentAlwaysSymlinkToDirectory() { s.Require().Equal(cfg.GenesisBin(), currentBin) s.assertCurrentLink(cfg, "genesis") - err = cfg.SetCurrentUpgrade("chain2") + err = cfg.SetCurrentUpgrade(cosmovisor.UpgradeInfo{Name: "chain2"}) s.Require().NoError(err) currentBin, err = cfg.CurrentBin() s.Require().NoError(err) @@ -100,7 +100,7 @@ func (s *upgradeTestSuite) TestDoUpgradeNoDownloadUrl() { // do upgrade ignores bad files for _, name := range []string{"missing", "nobin", "noexec"} { - info := &cosmovisor.UpgradeInfo{Name: name} + info := cosmovisor.UpgradeInfo{Name: name} err = cosmovisor.DoUpgrade(cfg, info) s.Require().Error(err, name) currentBin, err := cfg.CurrentBin() @@ -111,7 +111,7 @@ func (s *upgradeTestSuite) TestDoUpgradeNoDownloadUrl() { // make sure it updates a few times for _, upgrade := range []string{"chain2", "chain3"} { // now set it to a valid upgrade and make sure CurrentBin is now set properly - info := &cosmovisor.UpgradeInfo{Name: upgrade} + info := cosmovisor.UpgradeInfo{Name: upgrade} err = cosmovisor.DoUpgrade(cfg, info) s.Require().NoError(err) // we should see current point to the new upgrade dir @@ -130,30 +130,30 @@ func (s *upgradeTestSuite) TestOsArch() { func (s *upgradeTestSuite) TestGetDownloadURL() { // all download tests will fail if we are not on linux... - ref, err := filepath.Abs(filepath.FromSlash("./testdata/repo/ref_zipped")) + ref, err := filepath.Abs(filepath.FromSlash("./testdata/repo/ref_to_chain3-zip_dir.json")) s.Require().NoError(err) - badref, err := filepath.Abs(filepath.FromSlash("./testdata/repo/zip_binary/autod.zip")) + badref, err := filepath.Abs(filepath.FromSlash("./testdata/repo/chain2-zip_bin/autod.zip")) // "./testdata/repo/zip_binary/autod.zip")) s.Require().NoError(err) cases := map[string]struct { - info string - url string - isErr bool + info string + url string + err string }{ "missing": { - isErr: true, + err: "downloading reference link : invalid source string:", }, "follow reference": { info: ref, - url: "https://github.com/cosmos/cosmos-sdk/raw/aa5d6140ad4011bb33d472dca8246a0dcbe223ee/cosmovisor/testdata/repo/zip_directory/autod.zip?checksum=sha256:3784e4574cad69b67e34d4ea4425eff140063a3870270a301d6bb24a098a27ae", + url: "https://github.com/cosmos/cosmos-sdk/raw/robert/cosmvisor-file-watch/cosmovisor/testdata/repo/chain3-zip_dir/autod.zip?checksum=sha256:8951f52a0aea8617de0ae459a20daf704c29d259c425e60d520e363df0f166b4", }, "malformated reference target": { - info: badref, - isErr: true, + info: badref, + err: "upgrade info doesn't contain binary map", }, "missing link": { - info: "https://no.such.domain/exists.txt", - isErr: true, + info: "https://no.such.domain/exists.txt", + err: "dial tcp: lookup no.such.domain: no such host", }, "proper binary": { info: `{"binaries": {"linux/amd64": "https://foo.bar/", "windows/amd64": "https://something.else"}}`, @@ -168,19 +168,22 @@ func (s *upgradeTestSuite) TestGetDownloadURL() { url: "https://foo.bar/portable", }, "missing binary": { - info: `{"binaries": {"linux/arm": "https://foo.bar/"}}`, - isErr: true, + info: `{"binaries": {"linux/arm": "https://foo.bar/"}}`, + err: "cannot find binary for", }, } - for _, tc := range cases { - url, err := cosmovisor.GetDownloadURL(&cosmovisor.UpgradeInfo{Info: tc.info}) - if tc.isErr { - s.Require().Error(err) - } else { - s.Require().NoError(err) - s.Require().Equal(tc.url, url) - } + for name, tc := range cases { + s.Run(name, func() { + url, err := cosmovisor.GetDownloadURL(cosmovisor.UpgradeInfo{Info: tc.info}) + if tc.err != "" { + s.Require().Error(err) + s.Require().Contains(err.Error(), tc.err) + } else { + s.Require().NoError(err) + s.Require().Equal(tc.url, url) + } + }) } } @@ -245,7 +248,7 @@ func (s *upgradeTestSuite) TestDownloadBinary() { } upgrade := "amazonas" - info := &cosmovisor.UpgradeInfo{ + info := cosmovisor.UpgradeInfo{ Name: upgrade, Info: fmt.Sprintf(`{"binaries":{"%s": "%s"}}`, cosmovisor.OSArch(), url), } @@ -271,9 +274,7 @@ func (s *upgradeTestSuite) TestDownloadBinary() { // returns the directory (which can now be used as Config.Home) and modified safely func copyTestData(t *testing.T, subdir string) string { t.Helper() - tmpdir := t.TempDir() - require.NoError(t, copy.Copy(filepath.Join("testdata", subdir), tmpdir)) return tmpdir diff --git a/store/types/gas.go b/store/types/gas.go index fe72cc520701..20ec518e3f18 100644 --- a/store/types/gas.go +++ b/store/types/gas.go @@ -130,7 +130,7 @@ type infiniteGasMeter struct { consumed Gas } -// NewInfiniteGasMeter returns a reference to a new infiniteGasMeter. +// NewInfiniteGasMeter returns a new gas meter without a limit. func NewInfiniteGasMeter() GasMeter { return &infiniteGasMeter{ consumed: 0, diff --git a/store/types/store.go b/store/types/store.go index 8bb7037ea9f0..03af7c437533 100644 --- a/store/types/store.go +++ b/store/types/store.go @@ -50,13 +50,6 @@ type StoreUpgrades struct { Deleted []string `json:"deleted"` } -// UpgradeInfo defines height and name of the upgrade -// to ensure multistore upgrades happen only at matching height. -type UpgradeInfo struct { - Name string `json:"name"` - Height int64 `json:"height"` -} - // StoreRename defines a name change of a sub-store. // All data previously under a PrefixStore with OldKey will be copied // to a PrefixStore with NewKey, then deleted from OldKey store. diff --git a/x/upgrade/keeper/keeper_test.go b/x/upgrade/keeper/keeper_test.go index 04d3cd7c43fc..52467af77174 100644 --- a/x/upgrade/keeper/keeper_test.go +++ b/x/upgrade/keeper/keeper_test.go @@ -9,7 +9,6 @@ import ( tmproto "github.com/tendermint/tendermint/proto/tendermint/types" "github.com/cosmos/cosmos-sdk/simapp" - store "github.com/cosmos/cosmos-sdk/store/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" "github.com/cosmos/cosmos-sdk/x/upgrade/keeper" @@ -44,16 +43,17 @@ func (s *KeeperTestSuite) TestReadUpgradeInfoFromDisk() { _, err := s.app.UpgradeKeeper.ReadUpgradeInfoFromDisk() s.Require().NoError(err) - expected := store.UpgradeInfo{ + expected := types.Plan{ Name: "test_upgrade", Height: 100, } // create an upgrade info file - s.Require().NoError(s.app.UpgradeKeeper.DumpUpgradeInfoToDisk(expected.Height, expected.Name)) + s.Require().NoError(s.app.UpgradeKeeper.DumpUpgradeInfoToDisk(101, expected)) ui, err := s.app.UpgradeKeeper.ReadUpgradeInfoFromDisk() s.Require().NoError(err) + expected.Height = 101 s.Require().Equal(expected, ui) } diff --git a/x/upgrade/spec/01_concepts.md b/x/upgrade/spec/01_concepts.md index 8e48409cbb0b..fbf6138f7480 100644 --- a/x/upgrade/spec/01_concepts.md +++ b/x/upgrade/spec/01_concepts.md @@ -67,14 +67,7 @@ not defined on a per-module basis. Registering this `StoreLoader` is done via func UpgradeStoreLoader (upgradeHeight int64, storeUpgrades *store.StoreUpgrades) baseapp.StoreLoader ``` -If there's a planned upgrade and the upgrade height is reached, the old binary writes `UpgradeInfo` to the disk before panic'ing. - -```go -type UpgradeInfo struct { - Name string - Height int64 -} -``` +If there's a planned upgrade and the upgrade height is reached, the old binary writes `Plan` to the disk before panic'ing. This information is critical to ensure the `StoreUpgrades` happens smoothly at correct height and expected upgrade. It eliminiates the chances for the new binary to execute `StoreUpgrades` multiple diff --git a/x/upgrade/types/plan.go b/x/upgrade/types/plan.go index 9e4bc85ab8fe..81c754caaee4 100644 --- a/x/upgrade/types/plan.go +++ b/x/upgrade/types/plan.go @@ -7,6 +7,9 @@ import ( sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) +// UpgradeInfoFileName file to store upgrade information +const UpgradeInfoFilename = "upgrade-info.json" + func (p Plan) String() string { due := p.DueAt() return fmt.Sprintf(`Upgrade Plan diff --git a/x/upgrade/types/storeloader_test.go b/x/upgrade/types/storeloader_test.go index 1f3aae003694..1a07ac050cf9 100644 --- a/x/upgrade/types/storeloader_test.go +++ b/x/upgrade/types/storeloader_test.go @@ -68,8 +68,8 @@ func TestSetLoader(t *testing.T) { // set a temporary home dir homeDir := t.TempDir() - upgradeInfoFilePath := filepath.Join(homeDir, "upgrade-info.json") - upgradeInfo := &store.UpgradeInfo{ + upgradeInfoFilePath := filepath.Join(homeDir, UpgradeInfoFilename) + upgradeInfo := &Plan{ Name: "test", Height: upgradeHeight, }