feat(cosmovisor): add add-upgrade command (#16413)

This commit is contained in:
Julien Robert 2023-06-05 17:46:34 +02:00 committed by GitHub
parent 0f1bfea1ab
commit c91ad30f77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 152 additions and 53 deletions

View File

@ -38,6 +38,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
## Features
* [#12457](https://github.com/cosmos/cosmos-sdk/issues/12457) Add `cosmovisor pre-upgrade` command to manually add an upgrade to cosmovisor.
* [#15361](https://github.com/cosmos/cosmos-sdk/pull/15361) Add `cosmovisor config` command to display the configuration used by cosmovisor.
## Client Breaking Changes

View File

@ -73,10 +73,14 @@ The first argument passed to `cosmovisor` is the action for `cosmovisor` to take
* `help`, `--help`, or `-h` - Output `cosmovisor` help information and check your `cosmovisor` configuration.
* `run` - Run the configured binary using the rest of the provided arguments.
* `version` - Output the `cosmovisor` version and also run the binary with the `version` argument.
* `config` - Display the current `cosmovisor` configuration, that means displaying the environment variables value that `cosmovisor` is using.
* `add-upgrade` - Add an upgrade manually to `cosmovisor`.
All arguments passed to `cosmovisor run` will be passed to the application binary (as a subprocess). `cosmovisor` will return `/dev/stdout` and `/dev/stderr` of the subprocess as its own. For this reason, `cosmovisor run` cannot accept any command-line arguments other than those available to the application binary.
*Note: Use of `cosmovisor` without one of the action arguments is deprecated. For backwards compatibility, if the first argument is not an action argument, `run` is assumed. However, this fallback might be removed in future versions, so it is recommended that you always provide `run`.
:::warning
Use of `cosmovisor` without one of the action arguments is deprecated. For backwards compatibility, if the first argument is not an action argument, `run` is assumed. However, this fallback might be removed in future versions, so it is recommended that you always provide `run`.
:::
`cosmovisor` reads its configuration from environment variables:

View File

@ -0,0 +1,84 @@
package main
import (
"fmt"
"os"
"path"
"cosmossdk.io/log"
"cosmossdk.io/tools/cosmovisor"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
)
func NewAddUpgradeCmd() *cobra.Command {
addUpgrade := &cobra.Command{
Use: "add-upgrade [upgrade-name] [path to executable]",
Short: "Manually add upgrade binary to Cosmovisor",
SilenceUsage: true,
Args: cobra.ExactArgs(2),
RunE: AddUpgrade,
}
addUpgrade.Flags().Bool(cosmovisor.FlagForce, false, "overwrite existing upgrade binary")
return addUpgrade
}
// AddUpgrade adds upgrade info to manifest
func AddUpgrade(cmd *cobra.Command, args []string) error {
cfg, err := cosmovisor.GetConfigFromEnv()
if err != nil {
return err
}
logger := cmd.Context().Value(log.ContextKey).(log.Logger)
if cfg.DisableLogs {
logger = log.NewCustomLogger(zerolog.Nop())
}
upgradeName := args[0]
if len(upgradeName) == 0 {
return fmt.Errorf("upgrade name cannot be empty")
}
executablePath := args[1]
if _, err := os.Stat(executablePath); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("invalid executable path: %w", err)
}
return fmt.Errorf("failed to load executable path: %w", err)
}
// create upgrade dir
upgradeLocation := cfg.UpgradeDir(upgradeName)
if err := os.MkdirAll(path.Join(upgradeLocation, "bin"), 0o750); err != nil {
return fmt.Errorf("failed to create upgrade directory: %w", err)
}
// copy binary to upgrade dir
executableData, err := os.ReadFile(executablePath)
if err != nil {
return fmt.Errorf("failed to read binary: %w", err)
}
if _, err := os.Stat(cfg.UpgradeBin(upgradeName)); err == nil {
if force, _ := cmd.Flags().GetBool(cosmovisor.FlagForce); !force {
return fmt.Errorf("upgrade binary already exists at %s", cfg.UpgradeBin(upgradeName))
}
logger.Info(fmt.Sprintf("Overwriting %s for %s upgrade", executablePath, upgradeName))
} else if !os.IsNotExist(err) {
return fmt.Errorf("failed to check if upgrade binary exists: %w", err)
}
if err := os.WriteFile(cfg.UpgradeBin(upgradeName), executableData, 0o600); err != nil {
return fmt.Errorf("failed to write binary to location: %w", err)
}
logger.Info(fmt.Sprintf("Using %s for %s upgrade", executablePath, upgradeName))
logger.Info(fmt.Sprintf("Upgrade binary located at %s", cfg.UpgradeBin(upgradeName)))
return nil
}

View File

@ -1,8 +1,6 @@
package main
import (
"fmt"
"cosmossdk.io/tools/cosmovisor"
"github.com/spf13/cobra"
)
@ -17,7 +15,7 @@ var configCmd = &cobra.Command{
return err
}
fmt.Fprint(cmd.OutOrStdout(), cfg.DetailString())
cmd.Print(cfg.DetailString())
return nil
},
}

View File

@ -7,7 +7,6 @@ import (
"os"
"path/filepath"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
"cosmossdk.io/log"
@ -21,14 +20,13 @@ var initCmd = &cobra.Command{
Short: "Initialize a cosmovisor daemon home directory.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
logger := cmd.Context().Value(log.ContextKey).(*zerolog.Logger)
logger := cmd.Context().Value(log.ContextKey).(log.Logger)
return InitializeCosmovisor(logger, args)
},
}
// InitializeCosmovisor initializes the cosmovisor directories, current link, and initial executable.
func InitializeCosmovisor(logger *zerolog.Logger, args []string) error {
func InitializeCosmovisor(logger log.Logger, args []string) error {
if len(args) < 1 || len(args[0]) == 0 {
return errors.New("no <path to executable> provided")
}
@ -46,14 +44,14 @@ func InitializeCosmovisor(logger *zerolog.Logger, args []string) error {
return err
}
logger.Info().Msg("checking on the genesis/bin directory")
logger.Info("checking on the genesis/bin directory")
genBinExe := cfg.GenesisBin()
genBinDir, _ := filepath.Split(genBinExe)
genBinDir = filepath.Clean(genBinDir)
switch genBinDirInfo, genBinDirErr := os.Stat(genBinDir); {
case os.IsNotExist(genBinDirErr):
logger.Info().Msgf("creating directory (and any parents): %q", genBinDir)
mkdirErr := os.MkdirAll(genBinDir, 0o755)
logger.Info(fmt.Sprintf("creating directory (and any parents): %q", genBinDir))
mkdirErr := os.MkdirAll(genBinDir, 0o750)
if mkdirErr != nil {
return mkdirErr
}
@ -62,29 +60,29 @@ func InitializeCosmovisor(logger *zerolog.Logger, args []string) error {
case !genBinDirInfo.IsDir():
return fmt.Errorf("the path %q already exists but is not a directory", genBinDir)
default:
logger.Info().Msgf("the %q directory already exists", genBinDir)
logger.Info(fmt.Sprintf("the %q directory already exists", genBinDir))
}
logger.Info().Msg("checking on the genesis/bin executable")
logger.Info("checking on the genesis/bin executable")
if _, err = os.Stat(genBinExe); os.IsNotExist(err) {
logger.Info().Msgf("copying executable into place: %q", genBinExe)
logger.Info(fmt.Sprintf("copying executable into place: %q", genBinExe))
if cpErr := copyFile(pathToExe, genBinExe); cpErr != nil {
return cpErr
}
} else {
logger.Info().Msgf("the %q file already exists", genBinExe)
logger.Info(fmt.Sprintf("the %q file already exists", genBinExe))
}
logger.Info().Msgf("making sure %q is executable", genBinExe)
logger.Info(fmt.Sprintf("making sure %q is executable", genBinExe))
if err = plan.EnsureBinary(genBinExe); err != nil {
return err
}
logger.Info().Msg("checking on the current symlink and creating it if needed")
logger.Info("checking on the current symlink and creating it if needed")
cur, curErr := cfg.CurrentBin()
if curErr != nil {
return curErr
}
logger.Info().Msgf("the current symlink points to: %q", cur)
logger.Info(fmt.Sprintf("the current symlink points to: %q", cur))
return nil
}

View File

@ -14,6 +14,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"cosmossdk.io/log"
"cosmossdk.io/tools/cosmovisor"
)
@ -227,12 +228,12 @@ func (p *BufferedPipe) panicIfStarted(msg string) {
}
// NewCapturingLogger creates a buffered stdout pipe, and a logger that uses it.
func (s *InitTestSuite) NewCapturingLogger() (*BufferedPipe, *zerolog.Logger) {
func (s *InitTestSuite) NewCapturingLogger() (*BufferedPipe, log.Logger) {
bufferedStdOut, err := StartNewBufferedPipe("stdout", os.Stdout)
s.Require().NoError(err, "creating stdout buffered pipe")
output := zerolog.ConsoleWriter{Out: bufferedStdOut, TimeFormat: time.RFC3339Nano}
logger := zerolog.New(output).With().Str("module", "cosmovisor").Timestamp().Logger()
return &bufferedStdOut, &logger
logger := log.NewCustomLogger(zerolog.New(output).With().Str("module", "cosmovisor").Timestamp().Logger())
return &bufferedStdOut, logger
}
// CreateHelloWorld creates a shell script that outputs HELLO WORLD.
@ -348,9 +349,9 @@ func (s *InitTestSuite) TestInitializeCosmovisorInvalidExisting() {
require.NoError(t, copyFile(hwExe, genBin), "copying exe to genesis/bin")
s.setEnv(t, env)
logger := zerolog.Nop()
logger := log.NewNopLogger()
expErr := fmt.Sprintf("the path %q already exists but is not a directory", genBin)
err := InitializeCosmovisor(&logger, []string{hwExe})
err := InitializeCosmovisor(logger, []string{hwExe})
require.EqualError(t, err, expErr, "invalid path to executable: must not be a directory", "calling InitializeCosmovisor")
})
@ -379,7 +380,7 @@ func (s *InitTestSuite) TestInitializeCosmovisorInvalidExisting() {
s.setEnv(t, env)
buffer, logger := s.NewCapturingLogger()
logger.Info().Msgf("Calling InitializeCosmovisor: %s", t.Name())
logger.Info(fmt.Sprintf("Calling InitializeCosmovisor: %s", t.Name()))
err := InitializeCosmovisor(logger, []string{hwExe})
require.EqualError(t, err, expErr, "calling InitializeCosmovisor")
bufferBz := buffer.Collect()
@ -407,7 +408,7 @@ func (s *InitTestSuite) TestInitializeCosmovisorInvalidExisting() {
s.setEnv(t, env)
buffer, logger := s.NewCapturingLogger()
logger.Info().Msgf("Calling InitializeCosmovisor: %s", t.Name())
logger.Info(fmt.Sprintf("Calling InitializeCosmovisor: %s", t.Name()))
err := InitializeCosmovisor(logger, []string{hwExe})
require.EqualError(t, err, expErr, "calling InitializeCosmovisor")
bufferBz := buffer.Collect()
@ -462,7 +463,7 @@ func (s *InitTestSuite) TestInitializeCosmovisorValid() {
s.setEnv(s.T(), env)
buffer, logger := s.NewCapturingLogger()
logger.Info().Msgf("Calling InitializeCosmovisor: %s", t.Name())
logger.Info(fmt.Sprintf("Calling InitializeCosmovisor: %s", t.Name()))
err := InitializeCosmovisor(logger, []string{hwNonExe})
require.NoError(t, err, "calling InitializeCosmovisor")
@ -514,7 +515,7 @@ func (s *InitTestSuite) TestInitializeCosmovisorValid() {
s.setEnv(t, env)
buffer, logger := s.NewCapturingLogger()
logger.Info().Msgf("Calling InitializeCosmovisor: %s", t.Name())
logger.Info(fmt.Sprintf("Calling InitializeCosmovisor: %s", t.Name()))
err := InitializeCosmovisor(logger, []string{hwExe})
require.NoError(t, err, "calling InitializeCosmovisor")
bufferBz := buffer.Collect()
@ -546,7 +547,7 @@ func (s *InitTestSuite) TestInitializeCosmovisorValid() {
s.setEnv(t, env)
buffer, logger := s.NewCapturingLogger()
logger.Info().Msgf("Calling InitializeCosmovisor: %s", t.Name())
logger.Info(fmt.Sprintf("Calling InitializeCosmovisor: %s", t.Name()))
err := InitializeCosmovisor(logger, []string{hwExe})
require.NoError(t, err, "calling InitializeCosmovisor")
bufferBz := buffer.Collect()

View File

@ -16,6 +16,7 @@ func NewRootCmd() *cobra.Command {
runCmd,
configCmd,
NewVersionCmd(),
NewAddUpgradeCmd(),
)
return rootCmd

View File

@ -6,27 +6,26 @@ import (
"runtime/debug"
"strings"
"cosmossdk.io/tools/cosmovisor"
"github.com/spf13/cobra"
)
// OutputFlag defines the output format flag
var OutputFlag = "output"
func NewVersionCmd() *cobra.Command {
versionCmd := &cobra.Command{
Use: "version",
Short: "Display cosmovisor and APP version.",
SilenceUsage: true,
Use: "version",
Short: "Display cosmovisor and APP version.",
RunE: func(cmd *cobra.Command, args []string) error {
if val, err := cmd.Flags().GetString(OutputFlag); val == "json" && err == nil {
return printVersionJSON(cmd, args)
noAppVersion, _ := cmd.Flags().GetBool(cosmovisor.FlagNoAppVersion)
if val, err := cmd.Flags().GetString(cosmovisor.FlagOutput); val == "json" && err == nil {
return printVersionJSON(cmd, args, noAppVersion)
}
return printVersion(cmd, args)
return printVersion(cmd, args, noAppVersion)
},
}
versionCmd.Flags().StringP(OutputFlag, "o", "text", "Output format (text|json)")
versionCmd.Flags().StringP(cosmovisor.FlagOutput, "o", "text", "Output format (text|json)")
versionCmd.Flags().Bool(cosmovisor.FlagNoAppVersion, false, "Don't print APP version")
return versionCmd
}
@ -40,8 +39,11 @@ func getVersion() string {
return strings.TrimSpace(version.Main.Version)
}
func printVersion(cmd *cobra.Command, args []string) error {
fmt.Printf("cosmovisor version: %s\n", getVersion())
func printVersion(cmd *cobra.Command, args []string, noAppVersion bool) error {
cmd.Printf("cosmovisor version: %s\n", getVersion())
if noAppVersion {
return nil
}
if err := Run(cmd, append([]string{"version"}, args...)); err != nil {
return fmt.Errorf("failed to run version command: %w", err)
@ -50,9 +52,13 @@ func printVersion(cmd *cobra.Command, args []string) error {
return nil
}
func printVersionJSON(cmd *cobra.Command, args []string) error {
buf := new(strings.Builder)
func printVersionJSON(cmd *cobra.Command, args []string, noAppVersion bool) error {
if noAppVersion {
cmd.Printf(`{"cosmovisor_version":"%s"}`+"\n", getVersion())
return nil
}
buf := new(strings.Builder)
if err := Run(
cmd,
[]string{"version", "--long", "--output", "json"},
@ -72,6 +78,6 @@ func printVersionJSON(cmd *cobra.Command, args []string) error {
return fmt.Errorf("can't print version output, expected valid json from APP, got: %s - %w", buf.String(), err)
}
fmt.Println(string(out))
cmd.Println(string(out))
return nil
}

View File

@ -0,0 +1,8 @@
package cosmovisor
const (
FlagOutput = "output"
FlagSkipUpgradeHeight = "unsafe-skip-upgrades"
FlagNoAppVersion = "no-app-version"
FlagForce = "force"
)

View File

@ -78,7 +78,7 @@ func (l Launcher) Run(args []string, stdout, stderr io.Writer) (bool, error) {
return false, err
}
if err := UpgradeBinary(l.logger, l.cfg, l.fw.currentInfo); err != nil {
if err := UpgradeBinary(log.NewCustomLogger(*l.logger), l.cfg, l.fw.currentInfo); err != nil {
return false, err
}
@ -232,7 +232,7 @@ func IsSkipUpgradeHeight(args []string, upgradeInfo upgradetypes.Plan) bool {
func UpgradeSkipHeights(args []string) []int {
var heights []int
for i, arg := range args {
if arg == "--unsafe-skip-upgrades" {
if arg == fmt.Sprintf("--%s", FlagSkipUpgradeHeight) {
j := i + 1
for j < len(args) {

View File

@ -6,8 +6,7 @@ import (
"os"
"runtime"
"github.com/rs/zerolog"
"cosmossdk.io/log"
"cosmossdk.io/x/upgrade/plan"
upgradetypes "cosmossdk.io/x/upgrade/types"
)
@ -15,7 +14,7 @@ import (
// UpgradeBinary 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 UpgradeBinary(logger *zerolog.Logger, cfg *Config, p upgradetypes.Plan) error {
func UpgradeBinary(logger log.Logger, cfg *Config, p upgradetypes.Plan) error {
// simplest case is to switch the link
err := plan.EnsureBinary(cfg.UpgradeBin(p.Name))
if err == nil {
@ -55,11 +54,11 @@ func UpgradeBinary(logger *zerolog.Logger, cfg *Config, p upgradetypes.Plan) err
}
// If not there, then we try to download it... maybe
logger.Info().Msg("no upgrade binary found, beginning to download it")
logger.Info("no upgrade binary found, beginning to download it")
if err := plan.DownloadUpgrade(cfg.UpgradeDir(p.Name), url, cfg.Name); err != nil {
return fmt.Errorf("cannot download binary. %w", err)
}
logger.Info().Msg("downloading binary complete")
logger.Info("downloading binary complete")
// and then set the binary again
if err := plan.EnsureBinary(cfg.UpgradeBin(p.Name)); err != nil {

View File

@ -11,7 +11,6 @@ import (
"testing"
"github.com/otiai10/copy"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
@ -95,7 +94,7 @@ func (s *upgradeTestSuite) assertCurrentLink(cfg cosmovisor.Config, target strin
func (s *upgradeTestSuite) TestUpgradeBinaryNoDownloadUrl() {
home := copyTestData(s.T(), "validate")
cfg := &cosmovisor.Config{Home: home, Name: "dummyd", AllowDownloadBinaries: true}
logger := log.NewLogger(os.Stdout).With(log.ModuleKey, "cosmovisor").Impl().(*zerolog.Logger)
logger := log.NewLogger(os.Stdout).With(log.ModuleKey, "cosmovisor")
currentBin, err := cfg.CurrentBin()
s.Require().NoError(err)
@ -128,7 +127,7 @@ func (s *upgradeTestSuite) TestUpgradeBinaryNoDownloadUrl() {
}
func (s *upgradeTestSuite) TestUpgradeBinary() {
logger := log.NewLogger(os.Stdout).With(log.ModuleKey, "cosmovisor").Impl().(*zerolog.Logger)
logger := log.NewLogger(os.Stdout).With(log.ModuleKey, "cosmovisor")
cases := map[string]struct {
url string