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:
@@ -0,0 +1 @@
|
||||
/cosmovisor
|
||||
+16
-2
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!!!
|
||||
|
||||
+13
@@ -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
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo Chain 3 from zipped directory
|
||||
echo Args: $@
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Vendored
-5
@@ -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
@@ -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'
|
||||
BIN
Binary file not shown.
Binary file not shown.
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo Chain 2 from zipped directory
|
||||
echo Args: $@
|
||||
@@ -0,0 +1 @@
|
||||
{"name": "upgrade1", "info": "some info", "height": 123}
|
||||
@@ -0,0 +1 @@
|
||||
{"name": "upgrade1", "heigh": "123"}
|
||||
@@ -0,0 +1 @@
|
||||
{"name": "upgrade1", "info": 123, "heigh": 123}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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!!!
|
||||
|
||||
+6
-33
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user