feat: file watcher for cosmovisor (#8590)

Adding upgrade file watcher for cosmovisor.

Currently the comswisor upgrade mechanism relays on parsing log messages. This is not reliable:
+ depends on the log level output (x/upgrade uses INFO)
+ can be hacked by accidentally logging user user content
+ can be broken by using upgrade name which will break the regex pattern.

closes: #7703
closes: #8523
closes: #8651
closes: #8793
closes: #8964 

**Depends on**: 
- #9652

---

Before we can merge this PR, please make sure that all the following items have been
checked off. If any of the checklist items are not applicable, please leave them but
write a little note why.

- [ ] Targeted PR against correct branch (see [CONTRIBUTING.md](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting))
- [ ] Linked to Github issue with discussion and accepted design OR link to spec that describes this work.
- [ ] Code follows the [module structure standards](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules/structure.md).
- [ ] Wrote unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing)
- [ ] Updated relevant documentation (`docs/`) or specification (`x/<module>/spec/`)
- [ ] Added relevant `godoc` [comments](https://blog.golang.org/godoc-documenting-go-code).
- [ ] Added a relevant changelog entry to the `Unreleased` section in `CHANGELOG.md`
- [ ] Re-reviewed `Files changed` in the Github PR explorer
- [ ] Review `Codecov Report` in the comment section below once CI passes
This commit is contained in:
Robert Zaremba
2021-08-11 15:03:48 +00:00
committed by GitHub
parent 8f5120b9bb
commit 13559f9132
43 changed files with 592 additions and 424 deletions
+1
View File
@@ -0,0 +1 @@
/cosmovisor
+16 -2
View File
@@ -26,6 +26,7 @@ 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
@@ -40,8 +41,9 @@ All arguments passed to `cosmovisor` will be passed to the application binary (a
│   └── $DAEMON_NAME
└── upgrades
└── <name>
── 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/<name>`). 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/<name>`). The `name` variable in `upgrades/<name>` is the URI-encoded name of the upgrade as specified in the upgrade module plan.
@@ -71,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/<name>/bin` (where `<name>` 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.
+98 -18
View File
@@ -1,30 +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
@@ -45,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)
}
// Symlink to genesis
// UpgradeInfoFile 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)
@@ -67,24 +81,25 @@ func (cfg *Config) CurrentBin() (string, error) {
// if nothing here, fallback to genesis
info, err := os.Lstat(cur)
if err != nil {
//Create symlink to the genesis
// 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
// Create symlink to the genesis
return cfg.SymLinkToGenesis()
}
// resolve it
dest, err := os.Readlink(cur)
if err != nil {
//Create symlink to the genesis
// Create symlink to the genesis
return cfg.SymLinkToGenesis()
}
// 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
@@ -103,15 +118,15 @@ 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"
@@ -119,7 +134,6 @@ func GetConfigFromEnv() (*Config, error) {
if err := cfg.validate(); err != nil {
return nil, err
}
return cfg, nil
}
@@ -151,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
}
+34
View File
@@ -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()
}
+11 -4
View File
@@ -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,12 +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
}
+3 -3
View File
@@ -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
)
+6 -7
View File
@@ -61,21 +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 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
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=
+57 -119
View File
@@ -1,7 +1,6 @@
package cosmovisor
import (
"bufio"
"encoding/json"
"fmt"
"io"
@@ -12,52 +11,40 @@ import (
"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)
@@ -65,25 +52,52 @@ 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 err := doBackup(l.cfg); err != nil {
return false, err
}
if upgradeInfo != nil {
if err := doBackup(cfg); err != nil {
return true, DoUpgrade(l.cfg, l.fw.currentInfo)
}
// 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()
}()
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, DoUpgrade(cfg, upgradeInfo)
}
return false, nil
return true, nil
}
func doBackup(cfg *Config) error {
@@ -128,79 +142,3 @@ func doBackup(cfg *Config) error {
return 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
}
// 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
}
}
// 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
}
}
// 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
waitScan := func(scan *bufio.Scanner) {
upgrade, err := WaitForUpdate(scan)
if err != nil {
res.SetError(err)
} else if upgrade != nil {
res.SetUpgrade(upgrade)
// now we need to kill the process
_ = cmd.Process.Kill()
}
}
// wait for the scanners, which can trigger upgrade and kill cmd
go waitScan(scanOut)
go waitScan(scanErr)
// 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
}
// this will set the error code if it wasn't killed due to upgrade
res.SetError(err)
return res.AsResult()
}
+68 -53
View File
@@ -3,7 +3,7 @@
package cosmovisor_test
import (
"bytes"
"fmt"
"testing"
"github.com/stretchr/testify/suite"
@@ -22,93 +22,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", UnsafeSkipBackup: true}
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, UnsafeSkipBackup: 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)
}
+126 -30
View File
@@ -1,42 +1,138 @@
package cosmovisor
import (
"bufio"
"regexp"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"time"
)
// Trim off whitespace around the info - match least greedy, grab as much space on both sides
// Defined here: https://github.com/cosmos/cosmos-sdk/blob/release/v0.38.2/x/upgrade/abci.go#L38
// fmt.Sprintf("UPGRADE \"%s\" NEEDED at %s: %s", plan.Name, plan.DueAt(), plan.Info)
// DueAt defined here: https://github.com/cosmos/cosmos-sdk/blob/release/v0.38.2/x/upgrade/internal/types/plan.go#L73-L78
//
// if !p.Time.IsZero() {
// return fmt.Sprintf("time: %s", p.Time.UTC().Format(time.RFC3339))
// }
// return fmt.Sprintf("height: %d", p.Height)
var upgradeRegex = regexp.MustCompile(`UPGRADE "(.*)" NEEDED at ((height): (\d+)|(time): (\S+)):\s+(\S*)`)
// UpgradeInfo is the details from the regexp
// UpgradeInfo is the update details created by `x/upgrade/keeper.DumpUpgradeInfoToDisk`.
type UpgradeInfo struct {
Name string
Info string
Name string `json:"name"`
Info string `json:"info"`
Height uint `json:"height"`
}
// WaitForUpdate will listen to the scanner until a line matches upgradeRegexp.
// It returns (info, nil) on a matching line
// It returns (nil, err) if the input stream errored
// It returns (nil, nil) if the input closed without ever matching the regexp
func WaitForUpdate(scanner *bufio.Scanner) (*UpgradeInfo, error) {
for scanner.Scan() {
line := scanner.Text()
if upgradeRegex.MatchString(line) {
subs := upgradeRegex.FindStringSubmatch(line)
info := UpgradeInfo{
Name: subs[1],
Info: subs[7],
type fileWatcher struct {
// full path to a watched file
filename string
interval time.Duration
currentInfo UpgradeInfo
lastModTime time.Time
cancel chan bool
ticker *time.Ticker
needsUpdate bool
initialized bool
}
func newUpgradeFileWatcher(filename string, interval time.Duration) (*fileWatcher, error) {
if filename == "" {
return nil, errors.New("filename undefined")
}
filenameAbs, err := filepath.Abs(filename)
if err != nil {
return nil,
fmt.Errorf("wrong 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("wrong path, %s must be an existing directory, [%w]", dirname, err)
}
return &fileWatcher{filenameAbs, interval, UpgradeInfo{}, time.Time{}, make(chan bool), time.NewTicker(interval), false, false}, nil
}
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.
func (fw *fileWatcher) MonitorUpdate(currentUpgrade UpgradeInfo) <-chan struct{} {
fw.ticker.Reset(fw.interval)
done := make(chan struct{})
fw.cancel = make(chan bool)
fw.needsUpdate = false
go func() {
for {
select {
case <-fw.ticker.C:
if fw.CheckUpdate(currentUpgrade) {
done <- struct{}{}
return
}
case <-fw.cancel:
return
}
return &info, nil
}
}()
return done
}
// CheckUpdate reads update plan from file and checks if there is a new update request
// currentName is the name of currently running upgrade. The check is rejected if it finds
// an upgrade with the same name.
func (fw *fileWatcher) CheckUpdate(currentUpgrade UpgradeInfo) bool {
if fw.needsUpdate {
return true
}
stat, err := os.Stat(fw.filename)
if err != nil { // file doesn't exists
return false
}
if !stat.ModTime().After(fw.lastModTime) {
return false
}
info, err := parseUpgradeInfoFile(fw.filename)
if err != nil {
log.Fatal("Can't parse upgrade info file. Err: ", err)
return false
}
if !fw.initialized { // daemon has restarted
fw.initialized = true
fw.currentInfo = info
fw.lastModTime = stat.ModTime()
// heuristic: deamon has restarted, so we don't know if we successfully downloaded the upgrade or not.
// so we try to compare the running upgrade name (read from the cosmovisor file) with the upgrade info
if currentUpgrade.Name != fw.currentInfo.Name {
fw.needsUpdate = true
return true
}
}
return nil, scanner.Err()
if info.Height > fw.currentInfo.Height {
fw.currentInfo = info
fw.lastModTime = stat.ModTime()
fw.needsUpdate = true
return true
}
return false
}
func parseUpgradeInfoFile(filename string) (UpgradeInfo, error) {
var ui UpgradeInfo
f, err := os.Open(filename)
if err != nil {
return ui, err
}
defer f.Close()
d := json.NewDecoder(f)
err = d.Decode(&ui)
if err != nil {
return ui, err
}
// required values must be set
if ui.Height == 0 || ui.Name == "" {
return UpgradeInfo{}, fmt.Errorf("invalid upgrade-info.json content. Name and Hight must be not empty. Got: %v", ui)
}
return ui, err
}
+48 -49
View File
@@ -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",
},
},
}
}{{
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,
}}
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()
}()
// 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)
})
}
}
+9 -4
View File
@@ -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!!!
View File
+13
View File
@@ -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'
Binary file not shown.
Binary file not shown.
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
echo Chain 3 from zipped directory
echo Args: $@
+5
View File
@@ -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"
}
}
-5
View File
@@ -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"
}
}
-8
View File
@@ -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'
Binary file not shown.
Binary file not shown.
-4
View File
@@ -1,4 +0,0 @@
#!/bin/sh
echo Chain 2 from zipped directory
echo Args: $@
+1
View File
@@ -0,0 +1 @@
{"name": "upgrade1", "info": "some info", "height": 123}
+1
View File
@@ -0,0 +1 @@
{"name": "upgrade1", "heigh": "123"}
+1
View File
@@ -0,0 +1 @@
{"name": "upgrade1", "info": 123, "heigh": 123}
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
{"name": "upgrade2"}
@@ -0,0 +1 @@
{"height": 1}
@@ -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!!!
View File
+6 -33
View File
@@ -18,12 +18,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 {
@@ -37,7 +37,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
@@ -45,11 +45,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
@@ -102,7 +102,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 {
@@ -147,33 +147,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)
+32 -31
View File
@@ -36,7 +36,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)
@@ -45,15 +45,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)
}
}
@@ -66,7 +66,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)
@@ -99,7 +99,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()
@@ -110,7 +110,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
@@ -129,30 +129,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"}}`,
@@ -167,19 +167,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)
}
})
}
}
@@ -244,7 +247,7 @@ func (s *upgradeTestSuite) TestDownloadBinary() {
}
upgrade := "amazonas"
info := &cosmovisor.UpgradeInfo{
info := cosmovisor.UpgradeInfo{
Name: upgrade,
Info: fmt.Sprintf(`{"binaries":{"%s": "%s"}}`, cosmovisor.OSArch(), url),
}
@@ -270,9 +273,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
+1 -1
View File
@@ -149,7 +149,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,
-7
View File
@@ -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.
+7 -6
View File
@@ -26,33 +26,34 @@ func BeginBlocker(k keeper.Keeper, ctx sdk.Context, _ abci.RequestBeginBlock) {
if !found {
return
}
logger := ctx.Logger()
// To make sure clear upgrade is executed at the same block
if plan.ShouldExecute(ctx) {
// If skip upgrade has been set for current height, we clear the upgrade plan
if k.IsSkipHeight(ctx.BlockHeight()) {
skipUpgradeMsg := fmt.Sprintf("UPGRADE \"%s\" SKIPPED at %d: %s", plan.Name, plan.Height, plan.Info)
ctx.Logger().Info(skipUpgradeMsg)
logger.Info(skipUpgradeMsg)
// Clear the upgrade plan at current height
k.ClearUpgradePlan(ctx)
return
}
// Prepare shutdown if we don't have an upgrade handler for this upgrade name (meaning this software is out of date)
if !k.HasHandler(plan.Name) {
// Write the upgrade info to disk. The UpgradeStoreLoader uses this info to perform or skip
// store migrations.
err := k.DumpUpgradeInfoToDisk(ctx.BlockHeight(), plan.Name)
err := k.DumpUpgradeInfoToDisk(ctx.BlockHeight(), plan)
if err != nil {
panic(fmt.Errorf("unable to write upgrade info to filesystem: %s", err.Error()))
}
upgradeMsg := BuildUpgradeNeededMsg(plan)
// We don't have an upgrade handler for this upgrade name, meaning this software is out of date so shutdown
ctx.Logger().Error(upgradeMsg)
logger.Error(upgradeMsg)
panic(upgradeMsg)
}
// We have an upgrade handler for this upgrade name, so apply the upgrade
ctx.Logger().Info(fmt.Sprintf("applying upgrade \"%s\" at %s", plan.Name, plan.DueAt()))
ctx = ctx.WithBlockGasMeter(sdk.NewInfiniteGasMeter())
@@ -63,7 +64,7 @@ func BeginBlocker(k keeper.Keeper, ctx sdk.Context, _ abci.RequestBeginBlock) {
// if we have a pending upgrade, but it is not yet time, make sure we did not
// set the handler already
if k.HasHandler(plan.Name) {
downgradeMsg := fmt.Sprintf("BINARY UPDATED BEFORE TRIGGER! UPGRADE \"%s\" - in binary but not executed on chain", plan.Name)
downgradeMsg := fmt.Sprintf("BINARY UPDATED BEFORE TRIGGER! UPGRADE \"%s\" - in binary but not executed on chain. Downgrade your binary", plan.Name)
ctx.Logger().Error(downgradeMsg)
panic(downgradeMsg)
}
+19 -17
View File
@@ -4,7 +4,6 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"testing"
"time"
@@ -16,7 +15,6 @@ import (
dbm "github.com/tendermint/tm-db"
"github.com/cosmos/cosmos-sdk/simapp"
storetypes "github.com/cosmos/cosmos-sdk/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/types/module"
@@ -173,7 +171,7 @@ func VerifyCleared(t *testing.T, newCtx sdk.Context) {
t.Log("Verify that the upgrade plan has been cleared")
bz, err := s.querier(newCtx, []string{types.QueryCurrent}, abci.RequestQuery{})
require.NoError(t, err)
require.Nil(t, bz)
require.Nil(t, bz, string(bz))
}
func TestCanClear(t *testing.T) {
@@ -387,27 +385,31 @@ func TestUpgradeWithoutSkip(t *testing.T) {
func TestDumpUpgradeInfoToFile(t *testing.T) {
s := setupTest(10, map[int64]bool{})
require := require.New(t)
// require no error when the upgrade info file does not exist
_, err := s.keeper.ReadUpgradeInfoFromDisk()
require.NoError(err)
planHeight := s.ctx.BlockHeight() + 1
name := "test"
plan := types.Plan{
Name: "test",
Height: 0, // this should be overwritten by DumpUpgradeInfoToFile
}
t.Log("verify if upgrade height is dumped to file")
err := s.keeper.DumpUpgradeInfoToDisk(planHeight, name)
require.Nil(t, err)
err = s.keeper.DumpUpgradeInfoToDisk(planHeight, plan)
require.Nil(err)
upgradeInfoFilePath, err := s.keeper.GetUpgradeInfoPath()
require.Nil(t, err)
data, err := ioutil.ReadFile(upgradeInfoFilePath)
require.NoError(t, err)
var upgradeInfo storetypes.UpgradeInfo
err = json.Unmarshal(data, &upgradeInfo)
require.Nil(t, err)
upgradeInfo, err := s.keeper.ReadUpgradeInfoFromDisk()
require.NoError(err)
t.Log("Verify upgrade height from file matches ")
require.Equal(t, upgradeInfo.Height, planHeight)
require.Equal(upgradeInfo.Height, planHeight)
require.Equal(upgradeInfo.Name, plan.Name)
// clear the test file
upgradeInfoFilePath, err := s.keeper.GetUpgradeInfoPath()
require.Nil(err)
err = os.Remove(upgradeInfoFilePath)
require.Nil(t, err)
require.Nil(err)
}
+10 -10
View File
@@ -13,7 +13,6 @@ import (
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store/prefix"
store "github.com/cosmos/cosmos-sdk/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/types/module"
@@ -21,7 +20,8 @@ import (
"github.com/cosmos/cosmos-sdk/x/upgrade/types"
)
// UpgradeInfoFileName file to store upgrade information
// Deprecated: UpgradeInfoFileName file to store upgrade information
// use x/upgrade/types.UpgradeInfoFilename instead.
const UpgradeInfoFileName string = "upgrade-info.json"
type Keeper struct {
@@ -148,8 +148,7 @@ func (k Keeper) getModuleVersion(ctx sdk.Context, name string) (uint64, bool) {
}
// ScheduleUpgrade schedules an upgrade based on the specified plan.
// If there is another Plan already scheduled, it will overwrite it
// (implicitly cancelling the current plan)
// If there is another Plan already scheduled, it will cancel and overwrite it.
// ScheduleUpgrade will also write the upgraded client to the upgraded client path
// if an upgraded client is specified in the plan
func (k Keeper) ScheduleUpgrade(ctx sdk.Context, plan types.Plan) error {
@@ -314,15 +313,16 @@ func (k Keeper) IsSkipHeight(height int64) bool {
}
// DumpUpgradeInfoToDisk writes upgrade information to UpgradeInfoFileName.
func (k Keeper) DumpUpgradeInfoToDisk(height int64, name string) error {
func (k Keeper) DumpUpgradeInfoToDisk(height int64, p types.Plan) error {
upgradeInfoFilePath, err := k.GetUpgradeInfoPath()
if err != nil {
return err
}
upgradeInfo := store.UpgradeInfo{
Name: name,
upgradeInfo := types.Plan{
Name: p.Name,
Height: height,
Info: p.Info,
}
info, err := json.Marshal(upgradeInfo)
if err != nil {
@@ -340,7 +340,7 @@ func (k Keeper) GetUpgradeInfoPath() (string, error) {
return "", err
}
return filepath.Join(upgradeInfoFileDir, UpgradeInfoFileName), nil
return filepath.Join(upgradeInfoFileDir, types.UpgradeInfoFilename), nil
}
// getHomeDir returns the height at which the given upgrade was executed
@@ -352,8 +352,8 @@ func (k Keeper) getHomeDir() string {
// written to disk by the old binary when panicking. An error is returned if
// the upgrade path directory cannot be created or if the file exists and
// cannot be read or if the upgrade info fails to unmarshal.
func (k Keeper) ReadUpgradeInfoFromDisk() (store.UpgradeInfo, error) {
var upgradeInfo store.UpgradeInfo
func (k Keeper) ReadUpgradeInfoFromDisk() (types.Plan, error) {
var upgradeInfo types.Plan
upgradeInfoPath, err := k.GetUpgradeInfoPath()
if err != nil {
+3 -3
View File
@@ -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)
}
+1 -8
View File
@@ -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
+3
View File
@@ -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
+2 -2
View File
@@ -69,8 +69,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,
}