From 3f9c9a087760669c294fbe108b463eee8a561cf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Francisco=20L=C3=B3pez?= Date: Tue, 1 Oct 2024 20:28:21 +0200 Subject: [PATCH] feat(cosmovisor): Add prepare-upgrade cmd (#21972) --- tools/cosmovisor/CHANGELOG.md | 12 +- tools/cosmovisor/README.md | 33 +++++ tools/cosmovisor/args.go | 7 + .../cmd/cosmovisor/prepare_upgrade.go | 126 ++++++++++++++++++ tools/cosmovisor/cmd/cosmovisor/root.go | 1 + tools/cosmovisor/go.mod | 2 +- 6 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 tools/cosmovisor/cmd/cosmovisor/prepare_upgrade.go diff --git a/tools/cosmovisor/CHANGELOG.md b/tools/cosmovisor/CHANGELOG.md index ae0de6b86ffe..3db3e4ea9954 100644 --- a/tools/cosmovisor/CHANGELOG.md +++ b/tools/cosmovisor/CHANGELOG.md @@ -36,6 +36,10 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [Unreleased] +### Features + +* [#21972](https://github.com/cosmos/cosmos-sdk/pull/21972) Add `prepare-upgrade` command + ### Improvements * [#21462](https://github.com/cosmos/cosmos-sdk/pull/21462) Pass `stdin` to binary. @@ -50,10 +54,10 @@ Ref: https://keepachangelog.com/en/1.0.0/ * Bump `cosmossdk.io/x/upgrade` to v0.1.4 (including go-getter vulnerability fix) * [#19995](https://github.com/cosmos/cosmos-sdk/pull/19995): - * `init command` writes the configuration to the config file only at the default path `DAEMON_HOME/cosmovisor/config.toml`. - * Provide `--cosmovisor-config` flag with value as args to provide the path to the configuration file in the `run` command. `run --cosmovisor-config (other cmds with flags) ...`. - * Add `--cosmovisor-config` flag to provide `config.toml` path to the configuration file in root command used by `add-upgrade` and `config` subcommands. - * `config command` now displays the configuration from the config file if it is provided. If the config file is not provided, it will display the configuration from the environment variables. + * `init command` writes the configuration to the config file only at the default path `DAEMON_HOME/cosmovisor/config.toml`. + * Provide `--cosmovisor-config` flag with value as args to provide the path to the configuration file in the `run` command. `run --cosmovisor-config (other cmds with flags) ...`. + * Add `--cosmovisor-config` flag to provide `config.toml` path to the configuration file in root command used by `add-upgrade` and `config` subcommands. + * `config command` now displays the configuration from the config file if it is provided. If the config file is not provided, it will display the configuration from the environment variables. ## Bug Fixes diff --git a/tools/cosmovisor/README.md b/tools/cosmovisor/README.md index db15ab0a5d64..e726239a8969 100644 --- a/tools/cosmovisor/README.md +++ b/tools/cosmovisor/README.md @@ -18,6 +18,7 @@ It polls the `upgrade-info.json` file that is created by the x/upgrade module at * [Initialization](#initialization) * [Detecting Upgrades](#detecting-upgrades) * [Adding Upgrade Binary](#adding-upgrade-binary) + * [Preparing for an Upgrade](#preparing-for-an-upgrade) * [Auto-Download](#auto-download) * [Example: SimApp Upgrade](#example-simapp-upgrade) * [Chain Setup](#chain-setup) @@ -263,6 +264,38 @@ The result will look something like the following: `29139e1381b8177aec909fab9a75 You can also use `sha512sum` if you would prefer to use longer hashes, or `md5sum` if you would prefer to use broken hashes. Whichever you choose, make sure to set the hash algorithm properly in the checksum argument to the URL. +### Preparing for an Upgrade + +To prepare for an upgrade, use the `prepare-upgrade` command: + +```shell +cosmovisor prepare-upgrade +``` + +This command performs the following actions: + +1. Retrieves upgrade information directly from the blockchain about the next scheduled upgrade. +2. Downloads the new binary specified in the upgrade plan. +3. Verifies the binary's checksum (if required by configuration). +4. Places the new binary in the appropriate directory for Cosmovisor to use during the upgrade. + +The `prepare-upgrade` command provides detailed logging throughout the process, including: + +* The name and height of the upcoming upgrade +* The URL from which the new binary is being downloaded +* Confirmation of successful download and verification +* The path where the new binary has been placed + +Example output: + +```bash +INFO Preparing for upgrade name=v1.0.0 height=1000000 +INFO Downloading upgrade binary url=https://example.com/binary/v1.0.0?checksum=sha256:339911508de5e20b573ce902c500ee670589073485216bee8b045e853f24bce8 +INFO Upgrade preparation complete name=v1.0.0 height=1000000 +``` + +*Note: The current way of downloading manually and placing the binary at the right place would still work.* + ## Example: SimApp Upgrade The following instructions provide a demonstration of `cosmovisor` using the simulation application (`simapp`) shipped with the Cosmos SDK's source code. The following commands are to be run from within the `cosmos-sdk` repository. diff --git a/tools/cosmovisor/args.go b/tools/cosmovisor/args.go index be94fbadaf6c..6c6b5b7b0572 100644 --- a/tools/cosmovisor/args.go +++ b/tools/cosmovisor/args.go @@ -33,6 +33,7 @@ const ( EnvDataBackupPath = "DAEMON_DATA_BACKUP_DIR" EnvInterval = "DAEMON_POLL_INTERVAL" EnvPreupgradeMaxRetries = "DAEMON_PREUPGRADE_MAX_RETRIES" + EnvGRPCAddress = "DAEMON_GRPC_ADDRESS" EnvDisableLogs = "COSMOVISOR_DISABLE_LOGS" EnvColorLogs = "COSMOVISOR_COLOR_LOGS" EnvTimeFormatLogs = "COSMOVISOR_TIMEFORMAT_LOGS" @@ -63,6 +64,7 @@ type Config struct { UnsafeSkipBackup bool `toml:"unsafe_skip_backup" mapstructure:"unsafe_skip_backup" default:"false"` DataBackupPath string `toml:"daemon_data_backup_dir" mapstructure:"daemon_data_backup_dir"` PreUpgradeMaxRetries int `toml:"daemon_preupgrade_max_retries" mapstructure:"daemon_preupgrade_max_retries" default:"0"` + GRPCAddress string `toml:"daemon_grpc_address" mapstructure:"daemon_grpc_address"` DisableLogs bool `toml:"cosmovisor_disable_logs" mapstructure:"cosmovisor_disable_logs" default:"false"` ColorLogs bool `toml:"cosmovisor_color_logs" mapstructure:"cosmovisor_color_logs" default:"true"` TimeFormatLogs string `toml:"cosmovisor_timeformat_logs" mapstructure:"cosmovisor_timeformat_logs" default:"kitchen"` @@ -282,6 +284,11 @@ func GetConfigFromEnv(skipValidate bool) (*Config, error) { errs = append(errs, fmt.Errorf("%s could not be parsed to int: %w", EnvPreupgradeMaxRetries, err)) } + cfg.GRPCAddress = os.Getenv(EnvGRPCAddress) + if cfg.GRPCAddress == "" { + cfg.GRPCAddress = "localhost:9090" + } + if !skipValidate { errs = append(errs, cfg.validate()...) if len(errs) > 0 { diff --git a/tools/cosmovisor/cmd/cosmovisor/prepare_upgrade.go b/tools/cosmovisor/cmd/cosmovisor/prepare_upgrade.go new file mode 100644 index 000000000000..f15e88803a67 --- /dev/null +++ b/tools/cosmovisor/cmd/cosmovisor/prepare_upgrade.go @@ -0,0 +1,126 @@ +package main + +import ( + "context" + "crypto/tls" + "fmt" + "path/filepath" + "strings" + "time" + + "github.com/spf13/cobra" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + + "cosmossdk.io/tools/cosmovisor" + "cosmossdk.io/x/upgrade/plan" + upgradetypes "cosmossdk.io/x/upgrade/types" +) + +func NewPrepareUpgradeCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "prepare-upgrade", + Short: "Prepare for the next upgrade", + Long: `Prepare for the next upgrade by downloading and verifying the upgrade binary. +This command will query the chain for the current upgrade plan and download the specified binary. +gRPC must be enabled on the node for this command to work.`, + RunE: prepareUpgradeHandler, + SilenceUsage: false, + Args: cobra.NoArgs, + } + + return cmd +} + +func prepareUpgradeHandler(cmd *cobra.Command, _ []string) error { + configPath, err := cmd.Flags().GetString(cosmovisor.FlagCosmovisorConfig) + if err != nil { + return fmt.Errorf("failed to get config flag: %w", err) + } + + cfg, err := cosmovisor.GetConfigFromFile(configPath) + if err != nil { + return fmt.Errorf("failed to get config: %w", err) + } + + logger := cfg.Logger(cmd.OutOrStdout()) + + grpcAddress := cfg.GRPCAddress + logger.Info("Using gRPC address", "address", grpcAddress) + + upgradeInfo, err := queryUpgradeInfoFromChain(grpcAddress) + if err != nil { + return fmt.Errorf("failed to query upgrade info: %w", err) + } + + if upgradeInfo == nil { + logger.Info("No active upgrade plan found") + return nil + } + + logger.Info("Preparing for upgrade", "name", upgradeInfo.Name, "height", upgradeInfo.Height) + + upgradeInfoParsed, err := plan.ParseInfo(upgradeInfo.Info, plan.ParseOptionEnforceChecksum(cfg.DownloadMustHaveChecksum)) + if err != nil { + return fmt.Errorf("failed to parse upgrade info: %w", err) + } + + binaryURL, err := cosmovisor.GetBinaryURL(upgradeInfoParsed.Binaries) + if err != nil { + return fmt.Errorf("binary URL not found in upgrade plan. Cannot prepare for upgrade: %w", err) + } + + logger.Info("Downloading upgrade binary", "url", binaryURL) + + upgradeBin := filepath.Join(cfg.UpgradeBin(upgradeInfo.Name), cfg.Name) + if err := plan.DownloadUpgrade(filepath.Dir(upgradeBin), binaryURL, cfg.Name); err != nil { + return fmt.Errorf("failed to download and verify binary: %w", err) + } + + logger.Info("Upgrade preparation complete", "name", upgradeInfo.Name, "height", upgradeInfo.Height) + + return nil +} + +func queryUpgradeInfoFromChain(grpcAddress string) (*upgradetypes.Plan, error) { + if grpcAddress == "" { + return nil, fmt.Errorf("gRPC address is empty") + } + + grpcConn, err := getClient(grpcAddress) + if err != nil { + return nil, fmt.Errorf("failed to open gRPC client: %w", err) + } + defer grpcConn.Close() + + queryClient := upgradetypes.NewQueryClient(grpcConn) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + res, err := queryClient.CurrentPlan(ctx, &upgradetypes.QueryCurrentPlanRequest{}) + if err != nil { + return nil, fmt.Errorf("failed to query current upgrade plan: %w", err) + } + + return res.Plan, nil +} + +func getClient(endpoint string) (*grpc.ClientConn, error) { + var creds credentials.TransportCredentials + if strings.HasPrefix(endpoint, "https://") { + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS12, + } + creds = credentials.NewTLS(tlsConfig) + } else { + creds = insecure.NewCredentials() + } + + opts := []grpc.DialOption{ + grpc.WithTransportCredentials(creds), + } + + return grpc.NewClient(endpoint, opts...) +} diff --git a/tools/cosmovisor/cmd/cosmovisor/root.go b/tools/cosmovisor/cmd/cosmovisor/root.go index d8c351f4c143..5cd31f8aea5b 100644 --- a/tools/cosmovisor/cmd/cosmovisor/root.go +++ b/tools/cosmovisor/cmd/cosmovisor/root.go @@ -20,6 +20,7 @@ func NewRootCmd() *cobra.Command { NewVersionCmd(), NewAddUpgradeCmd(), NewShowUpgradeInfoCmd(), + NewPrepareUpgradeCmd(), ) rootCmd.PersistentFlags().StringP(cosmovisor.FlagCosmovisorConfig, "c", "", "path to cosmovisor config file") diff --git a/tools/cosmovisor/go.mod b/tools/cosmovisor/go.mod index 9ae2df793f3e..a4380890f1d4 100644 --- a/tools/cosmovisor/go.mod +++ b/tools/cosmovisor/go.mod @@ -10,6 +10,7 @@ require ( github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 + google.golang.org/grpc v1.67.0 ) require ( @@ -175,7 +176,6 @@ require ( google.golang.org/genproto v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 // indirect - google.golang.org/grpc v1.67.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect