From c91ad30f778b14d9e432eedb4a2ffe03cf097e37 Mon Sep 17 00:00:00 2001 From: Julien Robert Date: Mon, 5 Jun 2023 17:46:34 +0200 Subject: [PATCH] feat(cosmovisor): add `add-upgrade` command (#16413) --- tools/cosmovisor/CHANGELOG.md | 1 + tools/cosmovisor/README.md | 6 +- .../cosmovisor/cmd/cosmovisor/add_upgrade.go | 84 +++++++++++++++++++ tools/cosmovisor/cmd/cosmovisor/config.go | 4 +- tools/cosmovisor/cmd/cosmovisor/init.go | 26 +++--- tools/cosmovisor/cmd/cosmovisor/init_test.go | 21 ++--- tools/cosmovisor/cmd/cosmovisor/root.go | 1 + tools/cosmovisor/cmd/cosmovisor/version.go | 36 ++++---- tools/cosmovisor/flags.go | 8 ++ tools/cosmovisor/process.go | 4 +- tools/cosmovisor/upgrade.go | 9 +- tools/cosmovisor/upgrade_test.go | 5 +- 12 files changed, 152 insertions(+), 53 deletions(-) create mode 100644 tools/cosmovisor/cmd/cosmovisor/add_upgrade.go create mode 100644 tools/cosmovisor/flags.go diff --git a/tools/cosmovisor/CHANGELOG.md b/tools/cosmovisor/CHANGELOG.md index 9b66d909a4..59b93c7ce3 100644 --- a/tools/cosmovisor/CHANGELOG.md +++ b/tools/cosmovisor/CHANGELOG.md @@ -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 diff --git a/tools/cosmovisor/README.md b/tools/cosmovisor/README.md index 564040b33d..be2aded5db 100644 --- a/tools/cosmovisor/README.md +++ b/tools/cosmovisor/README.md @@ -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: diff --git a/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go new file mode 100644 index 0000000000..a7b90eb0dc --- /dev/null +++ b/tools/cosmovisor/cmd/cosmovisor/add_upgrade.go @@ -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 +} diff --git a/tools/cosmovisor/cmd/cosmovisor/config.go b/tools/cosmovisor/cmd/cosmovisor/config.go index 1a4aa22783..c6ef7af28a 100644 --- a/tools/cosmovisor/cmd/cosmovisor/config.go +++ b/tools/cosmovisor/cmd/cosmovisor/config.go @@ -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 }, } diff --git a/tools/cosmovisor/cmd/cosmovisor/init.go b/tools/cosmovisor/cmd/cosmovisor/init.go index dd49cce197..da5da92ec0 100644 --- a/tools/cosmovisor/cmd/cosmovisor/init.go +++ b/tools/cosmovisor/cmd/cosmovisor/init.go @@ -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 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 } diff --git a/tools/cosmovisor/cmd/cosmovisor/init_test.go b/tools/cosmovisor/cmd/cosmovisor/init_test.go index 471721b677..be943263d4 100644 --- a/tools/cosmovisor/cmd/cosmovisor/init_test.go +++ b/tools/cosmovisor/cmd/cosmovisor/init_test.go @@ -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() diff --git a/tools/cosmovisor/cmd/cosmovisor/root.go b/tools/cosmovisor/cmd/cosmovisor/root.go index e4ac242ddc..4d87d9867c 100644 --- a/tools/cosmovisor/cmd/cosmovisor/root.go +++ b/tools/cosmovisor/cmd/cosmovisor/root.go @@ -16,6 +16,7 @@ func NewRootCmd() *cobra.Command { runCmd, configCmd, NewVersionCmd(), + NewAddUpgradeCmd(), ) return rootCmd diff --git a/tools/cosmovisor/cmd/cosmovisor/version.go b/tools/cosmovisor/cmd/cosmovisor/version.go index 3cbbf72cad..a1c8e1bcb3 100644 --- a/tools/cosmovisor/cmd/cosmovisor/version.go +++ b/tools/cosmovisor/cmd/cosmovisor/version.go @@ -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 } diff --git a/tools/cosmovisor/flags.go b/tools/cosmovisor/flags.go new file mode 100644 index 0000000000..76a48d20da --- /dev/null +++ b/tools/cosmovisor/flags.go @@ -0,0 +1,8 @@ +package cosmovisor + +const ( + FlagOutput = "output" + FlagSkipUpgradeHeight = "unsafe-skip-upgrades" + FlagNoAppVersion = "no-app-version" + FlagForce = "force" +) diff --git a/tools/cosmovisor/process.go b/tools/cosmovisor/process.go index dea066e041..eff28e41d1 100644 --- a/tools/cosmovisor/process.go +++ b/tools/cosmovisor/process.go @@ -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) { diff --git a/tools/cosmovisor/upgrade.go b/tools/cosmovisor/upgrade.go index c42306332c..65d49bf821 100644 --- a/tools/cosmovisor/upgrade.go +++ b/tools/cosmovisor/upgrade.go @@ -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 { diff --git a/tools/cosmovisor/upgrade_test.go b/tools/cosmovisor/upgrade_test.go index 028e6b0d76..0e8ea169f2 100644 --- a/tools/cosmovisor/upgrade_test.go +++ b/tools/cosmovisor/upgrade_test.go @@ -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