Merge cosmosd (#6820)
Closes: #6521 Co-authored-by: Alexander Bezobchuk <alexanderbez@users.noreply.github.com> Co-authored-by: Alessio Treglia <alessio@tendermint.com> Co-authored-by: Anil Kumar Kammari <anil@vitwit.com>
This commit is contained in:
parent
8f51c150de
commit
bf0142b272
|
@ -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:
|
||||
|
|
|
@ -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/<name>
|
||||
├── genesis
|
||||
│ └── bin
|
||||
│ └── $DAEMON_NAME
|
||||
└── upgrades
|
||||
└── <name>
|
||||
└── bin
|
||||
└── $DAEMON_NAME
|
||||
```
|
||||
|
||||
Each version of the chain is stored under either `genesis` or `upgrades/<name>`, 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 `<name>` 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/<name>` 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/<name>` 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.
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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!!!
|
|
@ -0,0 +1,6 @@
|
|||
#!/bin/sh
|
||||
|
||||
echo Chain 2 is live!
|
||||
echo Args: $@
|
||||
sleep 1
|
||||
echo Finished successfully
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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'
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
|
||||
echo Chain 2 from zipped directory
|
||||
echo Args: $@
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
echo Genesis $@
|
||||
sleep 1
|
||||
echo 'UPGRADE "chain2" NEEDED at height: 49: {}'
|
||||
sleep 2
|
||||
echo Never should be printed!!!
|
|
@ -0,0 +1,6 @@
|
|||
#!/bin/sh
|
||||
|
||||
echo Chain 2 is live!
|
||||
echo Args: $@
|
||||
sleep 1
|
||||
echo Finished successfully
|
|
@ -0,0 +1,6 @@
|
|||
#!/bin/sh
|
||||
|
||||
echo Chain 3 finally!
|
||||
echo Args: $@
|
||||
sleep 1
|
||||
echo Finished successfully
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
echo 'exec flag not set'
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
26
go.sum
26
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=
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue