diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 14865c91d2..02573c2fa8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,6 +15,22 @@ jobs: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/master'" + test-cosmovisor: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: technote-space/get-diff-action@v1 + id: git_diff + with: + PREFIX_FILTER: | + cosmovisor + SUFFIX_FILTER: | + .go + .mod + .sum + - name: Run cosmovisor tests + run: cd cosmovisor; go test . + if: "env.GIT_DIFF != ''" split-test-files: runs-on: ubuntu-latest steps: diff --git a/cosmovisor/README.md b/cosmovisor/README.md new file mode 100644 index 0000000000..00e9a02719 --- /dev/null +++ b/cosmovisor/README.md @@ -0,0 +1,147 @@ +# Cosmovisor + +This is a tiny shim around Cosmos SDK binaries that use the upgrade +module that allows for smooth and configurable management of upgrading +binaries as a live chain is upgraded, and can be used to simplify validator +devops while doing upgrades or to make syncing a full node for genesis +simple. The `cosmovisor` will monitor the stdout of the daemon to look +for messages from the upgrade module indicating a pending or required upgrade +and act appropriately. (With better integrations possible in the future). + +## Arguments + +`cosmovisor` is a shim around a native binary. All arguments passed to the `cosmovisor` +command will be passed to the current daemon binary (as a subprocess). + It will return stdout and stderr of the subprocess as +it's own. Because of that, it cannot accept any command line arguments, nor +print anything to output (unless it dies before executing a binary). + +Configuration will be passed in the following environmental variables: + +* `DAEMON_HOME` is the location where upgrade binaries should be kept (can +be `$HOME/.gaiad` or `$HOME/.xrnd`) +* `DAEMON_NAME` is the name of the binary itself (eg. `xrnd`, `gaiad`, `simd`) +* `DAEMON_ALLOW_DOWNLOAD_BINARIES` (optional) if set to `true` will enable auto-downloading of new binaries +(for security reasons, this is intended for fullnodes rather than validators) +* `DAEMON_RESTART_AFTER_UPGRADE` (optional) if set to `true` it will restart the sub-process with the same args +(but new binary) after a successful upgrade. By default, the `cosmovisor` dies afterward and allows the cosmovisor +to restart it if needed. Note that this will not auto-restart the child if there was an error. + +## Folder Layout + +`$DAEMON_HOME/cosmovisor` is expected to belong completely to the cosmovisor and +subprocesses +controlled by it. Under this folder, we will see the following: + +``` +. +├── current -> genesis or upgrades/ +├── genesis +│   └── bin +│   └── $DAEMON_NAME +└── upgrades + └── + └── bin + └── $DAEMON_NAME +``` + +Each version of the chain is stored under either `genesis` or `upgrades/`, which holds `bin/$DAEMON_NAME` +along with any other needed files (maybe the cli client? maybe some dlls?). `current` is a symlink to the currently +active folder (so `current/bin/$DAEMON_NAME` is the binary) + +Note: the `` after `upgrades` is the URI-encoded name of the upgrade as specified in the upgrade module plan. + +Please note that `$DAEMON_HOME/cosmovisor` just stores the *binaries* and associated *program code*. +The `cosmovisor` binary can be stored in any typical location (eg `/usr/local/bin`). The actual blockchain +program will store it's data under `$GAIA_HOME` etc, which is independent of the `$DAEMON_HOME`. You can +choose to export `GAIA_HOME=$DAEMON_HOME` and then end up with a configuation like the following, but this +is left as a choice to the admin for best directory layout. + +``` +.gaiad +├── config +├── data +└── cosmovisor +``` + +## Usage + +Basic Usage: + +* The admin is responsible for installing the `cosmovisor` and setting it as a eg. systemd service to auto-restart, along with proper environmental variables +* The admin is responsible for installing the `genesis` folder manually +* The `cosmovisor` will set the `current` link to point to `genesis` at first start (when no `current` link exists) +* The admin is (generally) responsible for installing the `upgrades/` folders manually +* The `cosmovisor` handles switching over the binaries at the correct points, so the admin can prepare days in advance and relax at upgrade time + +Note that chains that wish to support upgrades may package up a genesis `cosmovisor` tar file with this info, just as they +prepare the genesis binary tar file. In fact, they may offer a tar file will all upgrades up to current point for easy download +for those who wish to sync a fullnode from start. + +The `DAEMON` specific code, like the tendermint config, the application db, syncing blocks, etc is done as normal. +The same eg. `GAIA_HOME` directives and command-line flags work, just the binary name is different. + +## Upgradeable Binary Specification + +In the basic version, the `cosmovisor` will read the stdout log messages +to determine when an upgrade is needed. We are considering more complex solutions +via signaling of some sort, but starting with the simple design: + +* when an upgrade is needed the binary will print a line that matches this +regular expression: `UPGRADE "(.*)" NEEDED at height (\d+):(.*)`. +* the second match in the above regular expression can be a JSON object with +a `binaries` key as described above + +The name (first regexp) will be used to select the new binary to run. If it is present, +the current subprocess will be killed, `current` will be upgraded to the new directory, +and the new binary will be launched. + +**Question** should we just kill the `cosmovisor` after it does the updates? +so it gets a clean restart and just runs the new binary (under `current`). +it should be safe to restart (as a service). + +## Auto-Download + +Generally, the system requires that the administrator place all relevant binaries +on the 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. + +If you set `DAEMON_ALLOW_DOWNLOAD_BINARIES=on` then when an upgrade is triggered and no local binary +can be found, the `cosmovisor` will attempt to download and install the binary itself. +The plan stored in the upgrade module has an info field for arbitrary json. +This info is expected to be outputed on the halt log message. There are two +valid format to specify a download in such a message: + +1. Store an os/architecture -> binary URI map in the upgrade plan info field +as JSON under the `"binaries"` key, eg: +```json +{ + "binaries": { + "linux/amd64":"https://example.com/gaia.zip?checksum=sha256:aec070645fe53ee3b3763059376134f058cc337247c978add178b6ccdfb0019f" + } +} +``` +2. Store a link to a file that contains all information in the above format (eg. if you want +to specify lots of binaries, changelog info, etc without filling up the blockchain). + +e.g `https://example.com/testnet-1001-info.json?checksum=sha256:deaaa99fda9407c4dbe1d04bd49bab0cc3c1dd76fa392cd55a9425be074af01e` + +This file contained in link will be retrieved by [go-getter](https://github.com/hashicorp/go-getter) +and the "binaries" field will be parsed as above. + +If there is no local binary, `DAEMON_ALLOW_DOWNLOAD_BINARIES=true`, and we can access a canonical url for the new binary, +then the `cosmovisor` will download it with [go-getter](https://github.com/hashicorp/go-getter) and +unpack it into the `upgrades/` folder to be run as if we installed it manually + +Note that for this mechanism to provide strong security guarantees, all URLs should include a +sha{256,512} checksum. This ensures that no false binary is run, even if someone hacks the server +or hijacks the dns. go-getter will always ensure the downloaded file matches the checksum if it +is provided. And also handles unpacking archives into directories (so these download links should be +a zip of all data in the bin directory). + +To properly create a checksum on linux, you can use the `sha256sum` utility. eg. +`sha256sum ./testdata/repo/zip_directory/autod.zip` +which should return `29139e1381b8177aec909fab9a75d11381cab5adf7d3af0c05ff1c9c117743a7`. +You can also use `sha512sum` if you like longer hashes, or `md5sum` if you like to use broken hashes. +Make sure to set the hash algorithm properly in the checksum argument to the url. diff --git a/cosmovisor/args.go b/cosmovisor/args.go new file mode 100644 index 0000000000..7e0ac2f548 --- /dev/null +++ b/cosmovisor/args.go @@ -0,0 +1,131 @@ +package cosmovisor + +import ( + "net/url" + "os" + "path/filepath" + + "github.com/pkg/errors" +) + +const ( + rootName = "cosmovisor" + genesisDir = "genesis" + upgradesDir = "upgrades" + currentLink = "current" +) + +// Config is the information passed in to control the daemon +type Config struct { + Home string + Name string + AllowDownloadBinaries bool + RestartAfterUpgrade bool +} + +// Root returns the root directory where all info lives +func (cfg *Config) Root() string { + return filepath.Join(cfg.Home, rootName) +} + +// GenesisBin is the path to the genesis binary - must be in place to start manager +func (cfg *Config) GenesisBin() string { + return filepath.Join(cfg.Root(), genesisDir, "bin", cfg.Name) +} + +// UpgradeBin is the path to the binary for the named upgrade +func (cfg *Config) UpgradeBin(upgradeName string) string { + return filepath.Join(cfg.UpgradeDir(upgradeName), "bin", cfg.Name) +} + +// UpgradeDir is the directory named upgrade +func (cfg *Config) UpgradeDir(upgradeName string) string { + safeName := url.PathEscape(upgradeName) + return filepath.Join(cfg.Root(), upgradesDir, safeName) +} + +// Symlink to genesis +func (cfg *Config) SymLinkToGenesis() (string, error) { + genesis := filepath.Join(cfg.Root(), genesisDir) + link := filepath.Join(cfg.Root(), currentLink) + + if err := os.Symlink(genesis, link); err != nil { + return "", err + } + // and return the genesis binary + return cfg.GenesisBin(), nil +} + +// CurrentBin is the path to the currently selected binary (genesis if no link is set) +// This will resolve the symlink to the underlying directory to make it easier to debug +func (cfg *Config) CurrentBin() (string, error) { + cur := filepath.Join(cfg.Root(), currentLink) + // if nothing here, fallback to genesis + info, err := os.Lstat(cur) + if err != nil { + //Create symlink to the genesis + return cfg.SymLinkToGenesis() + } + // if it is there, ensure it is a symlink + if info.Mode()&os.ModeSymlink == 0 { + //Create symlink to the genesis + return cfg.SymLinkToGenesis() + } + + // resolve it + dest, err := os.Readlink(cur) + if err != nil { + //Create symlink to the genesis + return cfg.SymLinkToGenesis() + } + + // and return the binary + dest = filepath.Join(dest, "bin", cfg.Name) + return dest, nil +} + +// GetConfigFromEnv will read the environmental variables into a config +// and then validate it is reasonable +func GetConfigFromEnv() (*Config, error) { + cfg := &Config{ + Home: os.Getenv("DAEMON_HOME"), + Name: os.Getenv("DAEMON_NAME"), + } + if os.Getenv("DAEMON_ALLOW_DOWNLOAD_BINARIES") == "true" { + cfg.AllowDownloadBinaries = true + } + if os.Getenv("DAEMON_RESTART_AFTER_UPGRADE") == "true" { + cfg.RestartAfterUpgrade = true + } + if err := cfg.validate(); err != nil { + return nil, err + } + return cfg, nil +} + +// validate returns an error if this config is invalid. +// it enforces Home/cosmovisor is a valid directory and exists, +// and that Name is set +func (cfg *Config) validate() error { + if cfg.Name == "" { + return errors.New("DAEMON_NAME is not set") + } + if cfg.Home == "" { + return errors.New("DAEMON_HOME is not set") + } + + if !filepath.IsAbs(cfg.Home) { + return errors.New("DAEMON_HOME must be an absolute path") + } + + // ensure the root directory exists + info, err := os.Stat(cfg.Root()) + if err != nil { + return errors.Wrap(err, "cannot stat home dir") + } + if !info.IsDir() { + return errors.Errorf("%s is not a directory", info.Name()) + } + + return nil +} diff --git a/cosmovisor/args_test.go b/cosmovisor/args_test.go new file mode 100644 index 0000000000..c36faacafa --- /dev/null +++ b/cosmovisor/args_test.go @@ -0,0 +1,130 @@ +package cosmovisor + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConfigPaths(t *testing.T) { + cases := map[string]struct { + cfg Config + upgradeName string + expectRoot string + expectGenesis string + expectUpgrade string + }{ + "simple": { + cfg: Config{Home: "/foo", Name: "myd"}, + upgradeName: "bar", + expectRoot: fmt.Sprintf("/foo/%s", rootName), + expectGenesis: fmt.Sprintf("/foo/%s/genesis/bin/myd", rootName), + expectUpgrade: fmt.Sprintf("/foo/%s/upgrades/bar/bin/myd", rootName), + }, + "handle space": { + cfg: Config{Home: "/longer/prefix/", Name: "yourd"}, + upgradeName: "some spaces", + expectRoot: fmt.Sprintf("/longer/prefix/%s", rootName), + expectGenesis: fmt.Sprintf("/longer/prefix/%s/genesis/bin/yourd", rootName), + expectUpgrade: "/longer/prefix/cosmovisor/upgrades/some%20spaces/bin/yourd", + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + assert.Equal(t, tc.cfg.Root(), filepath.FromSlash(tc.expectRoot)) + assert.Equal(t, tc.cfg.GenesisBin(), filepath.FromSlash(tc.expectGenesis)) + assert.Equal(t, tc.cfg.UpgradeBin(tc.upgradeName), filepath.FromSlash(tc.expectUpgrade)) + }) + } +} + +// Test validate +func TestValidate(t *testing.T) { + relPath := filepath.Join("testdata", "validate") + absPath, err := filepath.Abs(relPath) + assert.NoError(t, err) + + testdata, err := filepath.Abs("testdata") + assert.NoError(t, err) + + cases := map[string]struct { + cfg Config + valid bool + }{ + "happy": { + cfg: Config{Home: absPath, Name: "bind"}, + valid: true, + }, + "happy with download": { + cfg: Config{Home: absPath, Name: "bind", AllowDownloadBinaries: true}, + valid: true, + }, + "missing home": { + cfg: Config{Name: "bind"}, + valid: false, + }, + "missing name": { + cfg: Config{Home: absPath}, + valid: false, + }, + "relative path": { + cfg: Config{Home: relPath, Name: "bind"}, + valid: false, + }, + "no upgrade manager subdir": { + cfg: Config{Home: testdata, Name: "bind"}, + valid: false, + }, + "no such dir": { + cfg: Config{Home: filepath.FromSlash("/no/such/dir"), Name: "bind"}, + valid: false, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + err := tc.cfg.validate() + if tc.valid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} + +func TestEnsureBin(t *testing.T) { + relPath := filepath.Join("testdata", "validate") + absPath, err := filepath.Abs(relPath) + assert.NoError(t, err) + + cfg := Config{Home: absPath, Name: "dummyd"} + assert.NoError(t, cfg.validate()) + + err = EnsureBinary(cfg.GenesisBin()) + assert.NoError(t, err) + + cases := map[string]struct { + upgrade string + hasBin bool + }{ + "proper": {"chain2", true}, + "no binary": {"nobin", false}, + "not executable": {"noexec", false}, + "no directory": {"foobarbaz", false}, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + err := EnsureBinary(cfg.UpgradeBin(tc.upgrade)) + if tc.hasBin { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} diff --git a/cosmovisor/cmd/main.go b/cosmovisor/cmd/main.go new file mode 100644 index 0000000000..9a5cd7e0ad --- /dev/null +++ b/cosmovisor/cmd/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "fmt" + "os" + + cosmovisor "github.com/cosmos/cosmos-sdk/cosmovisor" +) + +func main() { + err := Run(os.Args[1:]) + if err != nil { + fmt.Printf("%+v\n", err) + os.Exit(1) + } +} + +// Run is the main loop, but returns an error +func Run(args []string) error { + cfg, err := cosmovisor.GetConfigFromEnv() + if err != nil { + return err + } + doUpgrade, err := cosmovisor.LaunchProcess(cfg, 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) + } + return err +} diff --git a/cosmovisor/go.mod b/cosmovisor/go.mod new file mode 100644 index 0000000000..d9f4fdfaec --- /dev/null +++ b/cosmovisor/go.mod @@ -0,0 +1,10 @@ +module github.com/cosmos/cosmos-sdk/cosmovisor + +go 1.14 + +require ( + github.com/hashicorp/go-getter v1.4.1 + github.com/otiai10/copy v1.2.0 + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.6.1 +) diff --git a/cosmovisor/go.sum b/cosmovisor/go.sum new file mode 100644 index 0000000000..f9861359c9 --- /dev/null +++ b/cosmovisor/go.sum @@ -0,0 +1,172 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1 h1:lRi0CHyU+ytlvylOlFKKq0af6JncuyoRh1J+QJBqQx0= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/aws/aws-sdk-go v1.15.78 h1:LaXy6lWR0YK7LKyuU0QWy2ws/LWTPfYV/UgfiBu4tvY= +github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/hashicorp/go-cleanhttp v0.5.0 h1:wvCrVc9TjDls6+YGAF2hAifE1E5U1+b4tH6KdvN3Gig= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-getter v1.4.1 h1:3A2Mh8smGFcf5M+gmcv898mZdrxpseik45IpcyISLsA= +github.com/hashicorp/go-getter v1.4.1/go.mod h1:7qxyCd8rBfcShwsvxgIguu4KbS3l8bUCwg2Umn7RjeY= +github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0= +github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= +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/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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/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= +go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0 h1:jbyannxz0XFD3zdjgrSUsaJbgpH4eTrkdhRChkHPfO8= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/cosmovisor/process.go b/cosmovisor/process.go new file mode 100644 index 0000000000..87acd8b7b4 --- /dev/null +++ b/cosmovisor/process.go @@ -0,0 +1,128 @@ +package cosmovisor + +import ( + "bufio" + "io" + "os/exec" + "strings" + "sync" + + "github.com/pkg/errors" +) + +// 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() + if err != nil { + return false, errors.Wrap(err, "error creating symlink to genesis") + } + err = EnsureBinary(bin) + if err != nil { + return false, errors.Wrap(err, "current binary invalid") + } + + 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)) + + err = cmd.Start() + if err != nil { + return false, errors.Wrapf(err, "launching process %s %s", bin, strings.Join(args, " ")) + } + + // three ways to exit - command ends, find regexp in scanOut, find regexp in scanErr + upgradeInfo, err := WaitForUpgradeOrExit(cmd, scanOut, scanErr) + if err != nil { + return false, err + } + if upgradeInfo != nil { + return true, DoUpgrade(cfg, upgradeInfo) + } + + return false, nil +} + +// WaitResult is used to wrap feedback on cmd state with some mutex logic. +// This is needed as multiple go-routines can affect this - two read pipes that can trigger upgrade +// As well as the command, which can fail +type WaitResult struct { + // both err and info may be updated from several go-routines + // access is wrapped by mutex and should only be done through methods + err error + info *UpgradeInfo + mutex sync.Mutex +} + +// AsResult reads the data protected by mutex to avoid race conditions +func (u *WaitResult) AsResult() (*UpgradeInfo, error) { + u.mutex.Lock() + defer u.mutex.Unlock() + return u.info, u.err +} + +// 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() +} diff --git a/cosmovisor/process_test.go b/cosmovisor/process_test.go new file mode 100644 index 0000000000..a8068f49c2 --- /dev/null +++ b/cosmovisor/process_test.go @@ -0,0 +1,110 @@ +// +build linux + +package cosmovisor + +import ( + "bytes" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestLaunchProcess will try running the script a few times and watch upgrades work properly +// and args are passed through +func TestLaunchProcess(t *testing.T) { + home, err := copyTestData("validate") + cfg := &Config{Home: home, Name: "dummyd"} + require.NoError(t, err) + defer os.RemoveAll(home) + + // should run the genesis binary and produce expected output + var stdout, stderr bytes.Buffer + currentBin, err := cfg.CurrentBin() + require.NoError(t, err) + + require.Equal(t, cfg.GenesisBin(), currentBin) + + args := []string{"foo", "bar", "1234"} + doUpgrade, err := LaunchProcess(cfg, args, &stdout, &stderr) + require.NoError(t, err) + assert.True(t, doUpgrade) + assert.Equal(t, "", stderr.String()) + assert.Equal(t, "Genesis foo bar 1234\nUPGRADE \"chain2\" NEEDED at height: 49: {}\n", stdout.String()) + + // ensure this is upgraded now and produces new output + + currentBin, err = cfg.CurrentBin() + require.NoError(t, err) + require.Equal(t, cfg.UpgradeBin("chain2"), currentBin) + args = []string{"second", "run", "--verbose"} + stdout.Reset() + stderr.Reset() + doUpgrade, err = LaunchProcess(cfg, args, &stdout, &stderr) + require.NoError(t, err) + assert.False(t, doUpgrade) + assert.Equal(t, "", stderr.String()) + assert.Equal(t, "Chain 2 is live!\nArgs: second run --verbose\nFinished successfully\n", stdout.String()) + + // ended without other upgrade + require.Equal(t, cfg.UpgradeBin("chain2"), currentBin) +} + +// TestLaunchProcess will try running the script a few times and watch upgrades work properly +// and args are passed through +func TestLaunchProcessWithDownloads(t *testing.T) { + // this is a fun path + // genesis -> "chain2" = zip_binary + // zip_binary -> "chain3" = ref_zipped -> zip_directory + // zip_directory no upgrade + home, err := copyTestData("download") + cfg := &Config{Home: home, Name: "autod", AllowDownloadBinaries: true} + require.NoError(t, err) + defer os.RemoveAll(home) + + // should run the genesis binary and produce expected output + var stdout, stderr bytes.Buffer + currentBin, err := cfg.CurrentBin() + require.NoError(t, err) + + require.Equal(t, cfg.GenesisBin(), currentBin) + args := []string{"some", "args"} + doUpgrade, err := LaunchProcess(cfg, args, &stdout, &stderr) + require.NoError(t, err) + assert.True(t, doUpgrade) + assert.Equal(t, "", stderr.String()) + assert.Equal(t, "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()) + + // ensure this is upgraded now and produces new output + currentBin, err = cfg.CurrentBin() + require.NoError(t, err) + require.Equal(t, cfg.UpgradeBin("chain2"), currentBin) + args = []string{"run", "--fast"} + stdout.Reset() + stderr.Reset() + doUpgrade, err = LaunchProcess(cfg, args, &stdout, &stderr) + require.NoError(t, err) + assert.True(t, doUpgrade) + assert.Equal(t, "", stderr.String()) + assert.Equal(t, "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()) + + // ended with one more upgrade + currentBin, err = cfg.CurrentBin() + require.NoError(t, err) + require.Equal(t, cfg.UpgradeBin("chain3"), currentBin) + // make sure this is the proper binary now.... + args = []string{"end", "--halt"} + stdout.Reset() + stderr.Reset() + doUpgrade, err = LaunchProcess(cfg, args, &stdout, &stderr) + require.NoError(t, err) + assert.False(t, doUpgrade) + assert.Equal(t, "", stderr.String()) + assert.Equal(t, "Chain 2 from zipped directory\nArgs: end --halt\n", stdout.String()) + + // and this doesn't upgrade + currentBin, err = cfg.CurrentBin() + require.NoError(t, err) + require.Equal(t, cfg.UpgradeBin("chain3"), currentBin) +} diff --git a/cosmovisor/scanner.go b/cosmovisor/scanner.go new file mode 100644 index 0000000000..81bca0717a --- /dev/null +++ b/cosmovisor/scanner.go @@ -0,0 +1,42 @@ +package cosmovisor + +import ( + "bufio" + "regexp" +) + +// 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 +type UpgradeInfo struct { + Name string + Info string +} + +// 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], + } + return &info, nil + } + } + return nil, scanner.Err() +} diff --git a/cosmovisor/scanner_test.go b/cosmovisor/scanner_test.go new file mode 100644 index 0000000000..a207006fe3 --- /dev/null +++ b/cosmovisor/scanner_test.go @@ -0,0 +1,61 @@ +package cosmovisor + +import ( + "bufio" + "io" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestWaitForInfo(t *testing.T) { + cases := map[string]struct { + write []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: &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: &UpgradeInfo{ + Name: "take2", + Info: "DownloadData", + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + r, w := io.Pipe() + scan := bufio.NewScanner(r) + + // write all info in separate routine + go func() { + for _, line := range tc.write { + n, err := w.Write([]byte(line)) + assert.NoError(t, err) + assert.Equal(t, len(line), n) + } + w.Close() + }() + + // now scan the info + info, err := WaitForUpdate(scan) + if tc.expectErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tc.expectUpgrade, info) + }) + } +} diff --git a/cosmovisor/testdata/download/cosmovisor/genesis/bin/autod b/cosmovisor/testdata/download/cosmovisor/genesis/bin/autod new file mode 100755 index 0000000000..113cce7f79 --- /dev/null +++ b/cosmovisor/testdata/download/cosmovisor/genesis/bin/autod @@ -0,0 +1,7 @@ +#!/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 Never should be printed!!! diff --git a/cosmovisor/testdata/repo/raw_binary/autod b/cosmovisor/testdata/repo/raw_binary/autod new file mode 100755 index 0000000000..0022b84af2 --- /dev/null +++ b/cosmovisor/testdata/repo/raw_binary/autod @@ -0,0 +1,6 @@ +#!/bin/sh + +echo Chain 2 is live! +echo Args: $@ +sleep 1 +echo Finished successfully diff --git a/cosmovisor/testdata/repo/ref_zipped b/cosmovisor/testdata/repo/ref_zipped new file mode 100644 index 0000000000..fd63c7161f --- /dev/null +++ b/cosmovisor/testdata/repo/ref_zipped @@ -0,0 +1,5 @@ +{ + "binaries": { + "linux/amd64": "https://github.com/cosmos/cosmos-sdk/raw/aa5d6140ad4011bb33d472dca8246a0dcbe223ee/cosmovisor/testdata/repo/zip_directory/autod.zip?checksum=sha256:3784e4574cad69b67e34d4ea4425eff140063a3870270a301d6bb24a098a27ae" + } +} \ No newline at end of file diff --git a/cosmovisor/testdata/repo/zip_binary/autod b/cosmovisor/testdata/repo/zip_binary/autod new file mode 100755 index 0000000000..4ed1dea36d --- /dev/null +++ b/cosmovisor/testdata/repo/zip_binary/autod @@ -0,0 +1,8 @@ +#!/bin/sh + +echo Chain 2 from zipped binary link to referral +echo Args: $@ +# note that we just have a url (follow the ref), not a full link +echo 'ERROR: UPGRADE "chain3" NEEDED at height: 936: https://github.com/cosmos/cosmos-sdk/raw/0eae1a50612b8bf803336d35055896fbddaa1ddd/cosmovisor/testdata/repo/ref_zipped?checksum=sha256:0a428575de718ed3cf0771c9687eefaf6f19359977eca4d94a0abd0e11ef8e64 module=main' +sleep 4 +echo 'Do not print' diff --git a/cosmovisor/testdata/repo/zip_binary/autod.zip b/cosmovisor/testdata/repo/zip_binary/autod.zip new file mode 100644 index 0000000000..0fe45f18f6 Binary files /dev/null and b/cosmovisor/testdata/repo/zip_binary/autod.zip differ diff --git a/cosmovisor/testdata/repo/zip_directory/autod.zip b/cosmovisor/testdata/repo/zip_directory/autod.zip new file mode 100644 index 0000000000..225cd4672a Binary files /dev/null and b/cosmovisor/testdata/repo/zip_directory/autod.zip differ diff --git a/cosmovisor/testdata/repo/zip_directory/bin/autod b/cosmovisor/testdata/repo/zip_directory/bin/autod new file mode 100755 index 0000000000..ea3a6f8311 --- /dev/null +++ b/cosmovisor/testdata/repo/zip_directory/bin/autod @@ -0,0 +1,4 @@ +#!/bin/sh + +echo Chain 2 from zipped directory +echo Args: $@ diff --git a/cosmovisor/testdata/validate/cosmovisor/genesis/bin/dummyd b/cosmovisor/testdata/validate/cosmovisor/genesis/bin/dummyd new file mode 100755 index 0000000000..c240b802a0 --- /dev/null +++ b/cosmovisor/testdata/validate/cosmovisor/genesis/bin/dummyd @@ -0,0 +1,7 @@ +#!/bin/sh + +echo Genesis $@ +sleep 1 +echo 'UPGRADE "chain2" NEEDED at height: 49: {}' +sleep 2 +echo Never should be printed!!! diff --git a/cosmovisor/testdata/validate/cosmovisor/upgrades/chain2/bin/dummyd b/cosmovisor/testdata/validate/cosmovisor/upgrades/chain2/bin/dummyd new file mode 100755 index 0000000000..0022b84af2 --- /dev/null +++ b/cosmovisor/testdata/validate/cosmovisor/upgrades/chain2/bin/dummyd @@ -0,0 +1,6 @@ +#!/bin/sh + +echo Chain 2 is live! +echo Args: $@ +sleep 1 +echo Finished successfully diff --git a/cosmovisor/testdata/validate/cosmovisor/upgrades/chain3/bin/dummyd b/cosmovisor/testdata/validate/cosmovisor/upgrades/chain3/bin/dummyd new file mode 100755 index 0000000000..edfb6c403e --- /dev/null +++ b/cosmovisor/testdata/validate/cosmovisor/upgrades/chain3/bin/dummyd @@ -0,0 +1,6 @@ +#!/bin/sh + +echo Chain 3 finally! +echo Args: $@ +sleep 1 +echo Finished successfully diff --git a/cosmovisor/testdata/validate/cosmovisor/upgrades/nobin/bin/.keep b/cosmovisor/testdata/validate/cosmovisor/upgrades/nobin/bin/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cosmovisor/testdata/validate/cosmovisor/upgrades/noexec/bin/dummyd b/cosmovisor/testdata/validate/cosmovisor/upgrades/noexec/bin/dummyd new file mode 100644 index 0000000000..9fa65cddeb --- /dev/null +++ b/cosmovisor/testdata/validate/cosmovisor/upgrades/noexec/bin/dummyd @@ -0,0 +1,3 @@ +#!/bin/sh + +echo 'exec flag not set' diff --git a/cosmovisor/upgrade.go b/cosmovisor/upgrade.go new file mode 100644 index 0000000000..12eadf0104 --- /dev/null +++ b/cosmovisor/upgrade.go @@ -0,0 +1,177 @@ +package cosmovisor + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/url" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/hashicorp/go-getter" + "github.com/pkg/errors" +) + +// 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 { + err := EnsureBinary(cfg.UpgradeBin(info.Name)) + + // Simplest case is to switch the link + if err == nil { + // we have the binary - do it + return cfg.SetCurrentUpgrade(info.Name) + } + + // if auto-download is disabled, we fail + if !cfg.AllowDownloadBinaries { + return errors.Wrap(err, "binary not present, downloading disabled") + } + // if the dir is there already, don't download either + _, err = os.Stat(cfg.UpgradeDir(info.Name)) + if !os.IsNotExist(err) { + return errors.Errorf("upgrade dir already exists, won't overwrite") + } + + // If not there, then we try to download it... maybe + if err := DownloadBinary(cfg, info); err != nil { + return errors.Wrap(err, "cannot download binary") + } + + // and then set the binary again + err = EnsureBinary(cfg.UpgradeBin(info.Name)) + if err != nil { + return errors.Wrap(err, "downloaded binary doesn't check out") + } + return cfg.SetCurrentUpgrade(info.Name) +} + +// DownloadBinary will grab the binary and place it in the proper directory +func DownloadBinary(cfg *Config, info *UpgradeInfo) error { + url, err := GetDownloadURL(info) + if err != nil { + return err + } + + // download into the bin dir (works for one file) + binPath := cfg.UpgradeBin(info.Name) + err = getter.GetFile(binPath, url) + + // if this fails, let's see if it is a zipped directory + if err != nil { + dirPath := cfg.UpgradeDir(info.Name) + err = getter.Get(dirPath, url) + } + if err != nil { + return err + } + // if it is successful, let's ensure the binary is executable + return MarkExecutable(binPath) +} + +// MarkExecutable will try to set the executable bits if not already set +// Fails if file doesn't exist or we cannot set those bits +func MarkExecutable(path string) error { + info, err := os.Stat(path) + if err != nil { + return errors.Wrap(err, "stating binary") + } + // end early if world exec already set + if info.Mode()&0001 == 1 { + return nil + } + // now try to set all exec bits + newMode := info.Mode().Perm() | 0111 + return os.Chmod(path, newMode) +} + +// UpgradeConfig is expected format for the info field to allow auto-download +type UpgradeConfig struct { + Binaries map[string]string `json:"binaries"` +} + +// GetDownloadURL will check if there is an arch-dependent binary specified in Info +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 { + tmpDir, err := ioutil.TempDir("", "upgrade-manager-reference") + if err != nil { + return "", errors.Wrap(err, "create tempdir for reference file") + } + defer os.RemoveAll(tmpDir) + refPath := filepath.Join(tmpDir, "ref") + err = getter.GetFile(refPath, doc) + if err != nil { + return "", errors.Wrapf(err, "downloading reference link %s", doc) + } + refBytes, err := ioutil.ReadFile(refPath) + if err != nil { + return "", errors.Wrap(err, "reading downloaded reference") + } + // if download worked properly, then we use this new file as the binary map to parse + doc = string(refBytes) + } + + // check if it is the upgrade config + var config UpgradeConfig + err := json.Unmarshal([]byte(doc), &config) + if err == nil { + url, ok := config.Binaries[osArch()] + if !ok { + return "", errors.Errorf("cannot find binary for os/arch: %s", osArch()) + } + return url, nil + } + + return "", errors.New("upgrade info doesn't contain binary map") +} + +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 errors.Wrap(err, "creating current symlink") + } + return nil +} + +// EnsureBinary ensures the file exists and is executable, or returns an error +func EnsureBinary(path string) error { + info, err := os.Stat(path) + if err != nil { + return errors.Wrap(err, "cannot stat home dir") + } + if !info.Mode().IsRegular() { + return errors.Errorf("%s is not a regular file", info.Name()) + } + // this checks if the world-executable bit is set (we cannot check owner easily) + exec := info.Mode().Perm() & 0001 + if exec == 0 { + return errors.Errorf("%s is not world executable", info.Name()) + } + return nil +} diff --git a/cosmovisor/upgrade_test.go b/cosmovisor/upgrade_test.go new file mode 100644 index 0000000000..cf84b6f662 --- /dev/null +++ b/cosmovisor/upgrade_test.go @@ -0,0 +1,284 @@ +// +build linux + +package cosmovisor + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + copy2 "github.com/otiai10/copy" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCurrentBin(t *testing.T) { + home, err := copyTestData("validate") + require.NoError(t, err) + defer os.RemoveAll(home) + + cfg := Config{Home: home, Name: "dummyd"} + + currentBin, err := cfg.CurrentBin() + require.NoError(t, err) + + assert.Equal(t, cfg.GenesisBin(), currentBin) + + // ensure we cannot set this to an invalid value + for _, name := range []string{"missing", "nobin", "noexec"} { + err = cfg.SetCurrentUpgrade(name) + require.Error(t, err, name) + + currentBin, err := cfg.CurrentBin() + require.NoError(t, err) + + assert.Equal(t, cfg.GenesisBin(), currentBin, name) + } + + // try a few times to make sure this can be reproduced + for _, upgrade := range []string{"chain2", "chain3", "chain2"} { + // now set it to a valid upgrade and make sure CurrentBin is now set properly + err = cfg.SetCurrentUpgrade(upgrade) + require.NoError(t, err) + // we should see current point to the new upgrade dir + currentBin, err := cfg.CurrentBin() + require.NoError(t, err) + + assert.Equal(t, cfg.UpgradeBin(upgrade), currentBin) + } +} + +func TestCurrentAlwaysSymlinkToDirectory(t *testing.T) { + home, err := copyTestData("validate") + require.NoError(t, err) + defer os.RemoveAll(home) + + cfg := Config{Home: home, Name: "dummyd"} + + currentBin, err := cfg.CurrentBin() + require.NoError(t, err) + assert.Equal(t, cfg.GenesisBin(), currentBin) + assertCurrentLink(t, cfg, "genesis") + + err = cfg.SetCurrentUpgrade("chain2") + require.NoError(t, err) + currentBin, err = cfg.CurrentBin() + require.NoError(t, err) + assert.Equal(t, cfg.UpgradeBin("chain2"), currentBin) + assertCurrentLink(t, cfg, filepath.Join("upgrades", "chain2")) +} + +func assertCurrentLink(t *testing.T, cfg Config, target string) { + link := filepath.Join(cfg.Root(), currentLink) + // ensure this is a symlink + info, err := os.Lstat(link) + require.NoError(t, err) + require.Equal(t, os.ModeSymlink, info.Mode()&os.ModeSymlink) + + dest, err := os.Readlink(link) + require.NoError(t, err) + expected := filepath.Join(cfg.Root(), target) + require.Equal(t, expected, dest) +} + +// TODO: test with download (and test all download functions) +func TestDoUpgradeNoDownloadUrl(t *testing.T) { + home, err := copyTestData("validate") + require.NoError(t, err) + defer os.RemoveAll(home) + + cfg := &Config{Home: home, Name: "dummyd", AllowDownloadBinaries: true} + + currentBin, err := cfg.CurrentBin() + require.NoError(t, err) + + assert.Equal(t, cfg.GenesisBin(), currentBin) + + // do upgrade ignores bad files + for _, name := range []string{"missing", "nobin", "noexec"} { + info := &UpgradeInfo{Name: name} + err = DoUpgrade(cfg, info) + require.Error(t, err, name) + currentBin, err := cfg.CurrentBin() + require.NoError(t, err) + assert.Equal(t, cfg.GenesisBin(), currentBin, name) + } + + // 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 := &UpgradeInfo{Name: upgrade} + err = DoUpgrade(cfg, info) + require.NoError(t, err) + // we should see current point to the new upgrade dir + upgradeBin := cfg.UpgradeBin(upgrade) + currentBin, err := cfg.CurrentBin() + require.NoError(t, err) + + assert.Equal(t, upgradeBin, currentBin) + } +} + +func TestOsArch(t *testing.T) { + // all download tests will fail if we are not on linux... + assert.Equal(t, "linux/amd64", osArch()) +} + +func TestGetDownloadURL(t *testing.T) { + // all download tests will fail if we are not on linux... + ref, err := filepath.Abs(filepath.FromSlash("./testdata/repo/ref_zipped")) + require.NoError(t, err) + badref, err := filepath.Abs(filepath.FromSlash("./testdata/repo/zip_binary/autod.zip")) + require.NoError(t, err) + + cases := map[string]struct { + info string + url string + isErr bool + }{ + "missing": { + isErr: true, + }, + "follow reference": { + info: ref, + url: "https://github.com/cosmos/cosmos-sdk/raw/aa5d6140ad4011bb33d472dca8246a0dcbe223ee/cosmovisor/testdata/repo/zip_directory/autod.zip?checksum=sha256:3784e4574cad69b67e34d4ea4425eff140063a3870270a301d6bb24a098a27ae", + }, + "malformated reference target": { + info: badref, + isErr: true, + }, + "missing link": { + info: "https://no.such.domain/exists.txt", + isErr: true, + }, + "proper binary": { + info: `{"binaries": {"linux/amd64": "https://foo.bar/", "windows/amd64": "https://something.else"}}`, + url: "https://foo.bar/", + }, + "missing binary": { + info: `{"binaries": {"linux/arm": "https://foo.bar/"}}`, + isErr: true, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + url, err := GetDownloadURL(&UpgradeInfo{Info: tc.info}) + if tc.isErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.url, url) + } + }) + } +} + +func TestDownloadBinary(t *testing.T) { + cases := map[string]struct { + url string + canDownload bool + validBinary bool + }{ + "get raw binary": { + url: "./testdata/repo/raw_binary/autod", + canDownload: true, + validBinary: true, + }, + "get raw binary with checksum": { + // sha256sum ./testdata/repo/raw_binary/autod + url: "./testdata/repo/raw_binary/autod?checksum=sha256:e6bc7851600a2a9917f7bf88eb7bdee1ec162c671101485690b4deb089077b0d", + canDownload: true, + validBinary: true, + }, + "get raw binary with invalid checksum": { + url: "./testdata/repo/raw_binary/autod?checksum=sha256:73e2bd6cbb99261733caf137015d5cc58e3f96248d8b01da68be8564989dd906", + canDownload: false, + }, + "get zipped directory": { + url: "./testdata/repo/zip_directory/autod.zip", + canDownload: true, + validBinary: true, + }, + "get zipped directory with valid checksum": { + // sha256sum ./testdata/repo/zip_directory/autod.zip + url: "./testdata/repo/zip_directory/autod.zip?checksum=sha256:3784e4574cad69b67e34d4ea4425eff140063a3870270a301d6bb24a098a27ae", + canDownload: true, + validBinary: true, + }, + "get zipped directory with invalid checksum": { + url: "./testdata/repo/zip_directory/autod.zip?checksum=sha256:73e2bd6cbb99261733caf137015d5cc58e3f96248d8b01da68be8564989dd906", + canDownload: false, + }, + "invalid url": { + url: "./testdata/repo/bad_dir/autod", + canDownload: false, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + // make temp dir + home, err := copyTestData("download") + require.NoError(t, err) + defer os.RemoveAll(home) + + cfg := &Config{ + Home: home, + Name: "autod", + AllowDownloadBinaries: true, + } + + // if we have a relative path, make it absolute, but don't change eg. https://... urls + url := tc.url + if strings.HasPrefix(url, "./") { + url, err = filepath.Abs(url) + require.NoError(t, err) + } + + upgrade := "amazonas" + info := &UpgradeInfo{ + Name: upgrade, + Info: fmt.Sprintf(`{"binaries":{"%s": "%s"}}`, osArch(), url), + } + + err = DownloadBinary(cfg, info) + if !tc.canDownload { + assert.Error(t, err) + return + } + require.NoError(t, err) + + err = EnsureBinary(cfg.UpgradeBin(upgrade)) + if tc.validBinary { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } +} + +// copyTestData will make a tempdir and then +// "cp -r" a subdirectory under testdata there +// returns the directory (which can now be used as Config.Home) and modified safely +func copyTestData(subdir string) (string, error) { + tmpdir, err := ioutil.TempDir("", "upgrade-manager-test") + if err != nil { + return "", errors.Wrap(err, "create temp dir") + } + + src := filepath.Join("testdata", subdir) + + err = copy2.Copy(src, tmpdir) + if err != nil { + os.RemoveAll(tmpdir) + return "", errors.Wrap(err, "copying files") + } + return tmpdir, nil +} diff --git a/go.sum b/go.sum index 029fe6c013..bf547d72d5 100644 --- a/go.sum +++ b/go.sum @@ -4,11 +4,16 @@ cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSR cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3 h1:AVXDdKsrtX33oR9fbCMu/+c1o8Ofjq6Ku/MInaLVg5Y= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1 h1:hL+ycaJpVE9M7nLoiXb/Pn10ENE2u+oddxbD8uu0ZVU= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0 h1:Kt+gOPPp2LEPWp8CSfxhsM8ik9CcyE/gYu+0r+RnZvM= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1 h1:W9tAK3E57P75u0XLLR82LZyw8VpAnhmyTOxW9qzmyj8= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0 h1:VV2nUM3wwLLGh9lSABFgZMjInyUbJeaRSE64WuAIQ+4= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/99designs/keyring v1.1.5 h1:wLv7QyzYpFIyMSwOADq1CLTF9KbjbBfcnfmOGJ64aO4= @@ -49,6 +54,7 @@ github.com/armon/go-metrics v0.3.4/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0 h1:0xphMHGMLBrPMfxR2AmVjZKcMEESEgWF8Kru94BNByk= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -177,12 +183,11 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekf github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -214,12 +219,14 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -254,6 +261,7 @@ github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyN github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -266,6 +274,7 @@ github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdv github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -285,6 +294,7 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= @@ -296,6 +306,7 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 h1:rBMNdlhTLzJjJSDIjNEXX1Pz3Hmwmz91v+zycvx9PJc= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= @@ -339,6 +350,7 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.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/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= @@ -550,6 +562,7 @@ go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -578,6 +591,7 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136 h1:A1gGSx58LAGVHUUsOf7IiR0u8Xb6W51gRwfDBhkdcaw= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -587,6 +601,7 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= @@ -619,6 +634,7 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrS golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -699,11 +715,13 @@ google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEt google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0 h1:Q3Ui3V3/CVinFWFiW39Iw0kMuVrRzYX0wN6OPFp0lTA= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -733,7 +751,6 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= @@ -780,9 +797,8 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/x/upgrade/abci.go b/x/upgrade/abci.go index 23645d6026..71a2609d22 100644 --- a/x/upgrade/abci.go +++ b/x/upgrade/abci.go @@ -41,7 +41,7 @@ func BeginBlocker(k keeper.Keeper, ctx sdk.Context, _ abci.RequestBeginBlock) { } if !k.HasHandler(plan.Name) { - upgradeMsg := fmt.Sprintf("UPGRADE \"%s\" NEEDED at %s: %s", plan.Name, plan.DueAt(), plan.Info) + 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) @@ -69,3 +69,8 @@ func BeginBlocker(k keeper.Keeper, ctx sdk.Context, _ abci.RequestBeginBlock) { panic(downgradeMsg) } } + +// BuildUpgradeNeededMsg prints the message that notifies that an upgrade is needed. +func BuildUpgradeNeededMsg(plan types.Plan) string { + return fmt.Sprintf("UPGRADE \"%s\" NEEDED at %s: %s", plan.Name, plan.DueAt(), plan.Info) +}