diff --git a/cosmovisor/process.go b/cosmovisor/process.go index 3059db9863..bc7b2e6133 100644 --- a/cosmovisor/process.go +++ b/cosmovisor/process.go @@ -10,6 +10,7 @@ import ( "os/exec" "os/signal" "path/filepath" + "strconv" "strings" "syscall" "time" @@ -64,6 +65,13 @@ func (l Launcher) Run(args []string, stdout, stderr io.Writer) (bool, error) { return false, err } + if !SkipUpgrade(args, l.fw.currentInfo) { + err = doPreUpgrade(l.cfg) + if err != nil { + return false, err + } + } + return true, DoUpgrade(l.cfg, l.fw.currentInfo) } @@ -143,3 +151,65 @@ func doBackup(cfg *Config) error { return nil } + +// doPreUpgrade runs the pre-upgrade command defined by the application +func doPreUpgrade(cfg *Config) error { + bin, err := cfg.CurrentBin() + preUpgradeCmd := exec.Command(bin, "pre-upgrade") + + _, err = preUpgradeCmd.Output() + + if err != nil { + if err.(*exec.ExitError).ProcessState.ExitCode() == 1 { + fmt.Println("pre-upgrade command does not exist. continuing the upgrade.") + return nil + } + if err.(*exec.ExitError).ProcessState.ExitCode() == 30 { + return fmt.Errorf("pre-upgrade command failed : %w", err) + } + if err.(*exec.ExitError).ProcessState.ExitCode() == 31 { + fmt.Println("pre-upgrade command failed. retrying.") + return doPreUpgrade(cfg) + } + } + fmt.Println("pre-upgrade successful. continuing the upgrade.") + return nil +} + +// skipUpgrade checks if pre-upgrade script must be run. If the height in the upgrade plan matches any of the heights provided in --safe-skip-upgrade, the script is not run +func SkipUpgrade(args []string, upgradeInfo UpgradeInfo) bool { + skipUpgradeHeights := UpgradeSkipHeights(args) + for _, h := range skipUpgradeHeights { + if h == int(upgradeInfo.Height) { + return true + } + + } + return false +} + +// UpgradeSkipHeights gets all the heights provided when +// simd start --unsafe-skip-upgrades ... +func UpgradeSkipHeights(args []string) []int { + var heights []int + for i, arg := range args { + if arg == "--unsafe-skip-upgrades" { + j := i + 1 + + for j < len(args) { + tArg := args[j] + if strings.HasPrefix(tArg, "-") { + break + } + h, err := strconv.Atoi(tArg) + if err == nil { + heights = append(heights, h) + } + j++ + } + + break + } + } + return heights +} diff --git a/cosmovisor/process_test.go b/cosmovisor/process_test.go index 21e0fe1200..d2e9794fa2 100644 --- a/cosmovisor/process_test.go +++ b/cosmovisor/process_test.go @@ -6,6 +6,7 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/cosmos/cosmos-sdk/cosmovisor" @@ -127,3 +128,78 @@ func (s *processTestSuite) TestLaunchProcessWithDownloads() { require.NoError(err) require.Equal(cfg.UpgradeBin("chain3"), currentBin) } + +// TestSkipUpgrade tests heights that are identified to be skipped and return if upgrade height matches the skip heights +func TestSkipUpgrade(t *testing.T) { + cases := []struct { + args []string + upgradeInfo cosmovisor.UpgradeInfo + expectRes bool + }{{ + args: []string{"appb", "start", "--unsafe-skip-upgrades"}, + upgradeInfo: cosmovisor.UpgradeInfo{Name: "upgrade1", Info: "some info", Height: 123}, + expectRes: false, + }, { + args: []string{"appb", "start", "--unsafe-skip-upgrades", "--abcd"}, + upgradeInfo: cosmovisor.UpgradeInfo{Name: "upgrade1", Info: "some info", Height: 123}, + expectRes: false, + }, { + args: []string{"appb", "start", "--unsafe-skip-upgrades", "10", "--abcd"}, + upgradeInfo: cosmovisor.UpgradeInfo{Name: "upgrade1", Info: "some info", Height: 11}, + expectRes: false, + }, { + args: []string{"appb", "start", "--unsafe-skip-upgrades", "10", "20", "--abcd"}, + upgradeInfo: cosmovisor.UpgradeInfo{Name: "upgrade1", Info: "some info", Height: 20}, + expectRes: true, + }, { + args: []string{"appb", "start", "--unsafe-skip-upgrades", "10", "20", "--abcd", "34"}, + upgradeInfo: cosmovisor.UpgradeInfo{Name: "upgrade1", Info: "some info", Height: 34}, + expectRes: false, + }} + + for i := range cases { + tc := cases[i] + require := require.New(t) + h := cosmovisor.SkipUpgrade(tc.args, tc.upgradeInfo) + require.Equal(h, tc.expectRes) + } +} + +// TestUpgradeSkipHeights tests if correct skip upgrade heights are identified from the cli args +func TestUpgradeSkipHeights(t *testing.T) { + cases := []struct { + args []string + expectRes []int + }{{ + args: []string{}, + expectRes: nil, + }, { + args: []string{"appb", "start"}, + expectRes: nil, + }, { + args: []string{"appb", "start", "--unsafe-skip-upgrades"}, + expectRes: nil, + }, { + args: []string{"appb", "start", "--unsafe-skip-upgrades", "--abcd"}, + expectRes: nil, + }, { + args: []string{"appb", "start", "--unsafe-skip-upgrades", "10", "--abcd"}, + expectRes: []int{10}, + }, { + args: []string{"appb", "start", "--unsafe-skip-upgrades", "10", "20", "--abcd"}, + expectRes: []int{10, 20}, + }, { + args: []string{"appb", "start", "--unsafe-skip-upgrades", "10", "20", "--abcd", "34"}, + expectRes: []int{10, 20}, + }, { + args: []string{"appb", "start", "--unsafe-skip-upgrades", "10", "as", "20", "--abcd"}, + expectRes: []int{10, 20}, + }} + + for i := range cases { + tc := cases[i] + require := require.New(t) + h := cosmovisor.UpgradeSkipHeights(tc.args) + require.Equal(h, tc.expectRes) + } +} diff --git a/docs/migrations/README.md b/docs/migrations/README.md index 9b122044ec..8c06882300 100644 --- a/docs/migrations/README.md +++ b/docs/migrations/README.md @@ -9,6 +9,7 @@ parent: This folder contains all the migration guides to update your app and modules to Cosmos v0.40 Stargate. 1. [App and Modules Migration](./app_and_modules.md) +1. [Pre Upgrade](./pre-upgrade.md) 1. [Chain Upgrade Guide to v0.40](./chain-upgrade-guide-040.md) 1. [REST Endpoints Migration](./rest.md) 1. [Keyring Migration](./keyring.md) diff --git a/docs/migrations/pre-upgrade.md b/docs/migrations/pre-upgrade.md new file mode 100644 index 0000000000..524867bfe8 --- /dev/null +++ b/docs/migrations/pre-upgrade.md @@ -0,0 +1,57 @@ +# Pre-Upgrade Handling + +Cosmovisor supports custom pre-upgrade handling. Use pre-upgrade handling when you need to implement application config changes that are required in the newer version before you perform the upgrade. + +Using Cosmovisor pre-upgrade handling is optional. If pre-upgrade handling is not implemented, the upgrade continues. + +For example, make the required new-version changes to `app.toml` settings during the pre-upgrade handling. The pre-upgrade handling process means that the file does not have to be manually updated after the upgrade. + +Before the application binary is upgraded, Cosmovisor calls a `pre-upgrade` command that can be implemented by the application. + +The `pre-upgrade` command does not take in any command-line arguments and is expected to terminate with the following exit codes: + + +| Exit status code | How it is handled in Cosmosvisor | +|------------------|---------------------------------------------------------------------------------------------------------------------| +| `0` | Assumes `pre-upgrade` command executed successfully and continues the upgrade. | +| `1` | Default exit code when `pre-upgrade` command has not been implemented. | +| `30` | `pre-upgrade` command was executed but failed. This fails the entire upgrade. | +| `31` | `pre-upgrade` command was executed but failed. But the command is retried until exit code `1` or `30` are returned. | + + +## Sample + +Here is a sample structure of the `pre-upgrade` command: + +```go +func preUpgradeCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "pre-upgrade", + Short: "Pre-upgrade command", + Long: "Pre-upgrade command to implement custom pre-upgrade handling", + Run: func(cmd *cobra.Command, args []string) { + + err := HandlePreUpgrade() + + if err != nil { + os.Exit(30) + } + + os.Exit(0) + + }, + } + + return cmd +} +``` + + +Ensure that the pre-upgrade command has been registered in the application: +```go +rootCmd.AddCommand( + // .. + preUpgradeCommand(), + // .. + ) +```