Skip to content

Commit

Permalink
feat(tools/cosmovisor): cosmovisor batch upgrades (#21790)
Browse files Browse the repository at this point in the history
Co-authored-by: Julien Robert <[email protected]>
  • Loading branch information
psiphi5 and julienrbrt authored Oct 10, 2024
1 parent 7e51736 commit 5b53cca
Show file tree
Hide file tree
Showing 7 changed files with 354 additions and 28 deletions.
1 change: 1 addition & 0 deletions tools/cosmovisor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Ref: https://keepachangelog.com/en/1.0.0/

### Features

* [#21790](https://github.com/cosmos/cosmos-sdk/pull/21790) Add `add-batch-upgrade` command.
* [#21972](https://github.com/cosmos/cosmos-sdk/pull/21972) Add `prepare-upgrade` command

### Improvements
Expand Down
5 changes: 5 additions & 0 deletions tools/cosmovisor/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ func (cfg *Config) UpgradeInfoFilePath() string {
return filepath.Join(cfg.Home, "data", upgradetypes.UpgradeInfoFilename)
}

// UpgradeInfoBatchFilePath is the same as UpgradeInfoFilePath but with a batch suffix.
func (cfg *Config) UpgradeInfoBatchFilePath() string {
return cfg.UpgradeInfoFilePath() + ".batch"
}

// SymLinkToGenesis creates a symbolic link from "./current" to the genesis directory.
func (cfg *Config) SymLinkToGenesis() (string, error) {
// workdir is set to cosmovisor directory so relative
Expand Down
69 changes: 43 additions & 26 deletions tools/cosmovisor/cmd/cosmovisor/add_upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func NewAddUpgradeCmd() *cobra.Command {
Short: "Add APP upgrade binary to cosmovisor",
SilenceUsage: true,
Args: cobra.ExactArgs(2),
RunE: AddUpgrade,
RunE: addUpgradeCmd,
}

addUpgrade.Flags().Bool(cosmovisor.FlagForce, false, "overwrite existing upgrade binary / upgrade-info.json file")
Expand All @@ -28,26 +28,14 @@ func NewAddUpgradeCmd() *cobra.Command {
return addUpgrade
}

// AddUpgrade adds upgrade info to manifest
func AddUpgrade(cmd *cobra.Command, args []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 err
}

// addUpgrade adds upgrade info to manifest
func addUpgrade(cfg *cosmovisor.Config, force bool, upgradeHeight int64, upgradeName, executablePath, upgradeInfoPath string) error {
logger := cfg.Logger(os.Stdout)

upgradeName := args[0]
if !cfg.DisableRecase {
upgradeName = strings.ToLower(args[0])
upgradeName = strings.ToLower(upgradeName)
}

executablePath := args[1]
if _, err := os.Stat(executablePath); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("invalid executable path: %w", err)
Expand All @@ -68,21 +56,14 @@ func AddUpgrade(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to read binary: %w", err)
}

force, err := cmd.Flags().GetBool(cosmovisor.FlagForce)
if err != nil {
return fmt.Errorf("failed to get force flag: %w", err)
}

if err := saveOrAbort(cfg.UpgradeBin(upgradeName), executableData, force); err != nil {
return err
}

logger.Info(fmt.Sprintf("Using %s for %s upgrade", executablePath, upgradeName))
logger.Info(fmt.Sprintf("Upgrade binary located at %s", cfg.UpgradeBin(upgradeName)))

if upgradeHeight, err := cmd.Flags().GetInt64(cosmovisor.FlagUpgradeHeight); err != nil {
return fmt.Errorf("failed to get upgrade-height flag: %w", err)
} else if upgradeHeight > 0 {
if upgradeHeight > 0 {
plan := upgradetypes.Plan{Name: upgradeName, Height: upgradeHeight}
if err := plan.ValidateBasic(); err != nil {
panic(fmt.Errorf("something is wrong with cosmovisor: %w", err))
Expand All @@ -94,16 +75,52 @@ func AddUpgrade(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to marshal upgrade plan: %w", err)
}

if err := saveOrAbort(cfg.UpgradeInfoFilePath(), planData, force); err != nil {
if err := saveOrAbort(upgradeInfoPath, planData, force); err != nil {
return err
}

logger.Info(fmt.Sprintf("%s created, %s upgrade binary will switch at height %d", cfg.UpgradeInfoFilePath(), upgradeName, upgradeHeight))
logger.Info(fmt.Sprintf("%s created, %s upgrade binary will switch at height %d", upgradeInfoPath, upgradeName, upgradeHeight))
}

return nil
}

// GetConfig returns a Config using passed-in flag
func getConfigFromCmd(cmd *cobra.Command) (*cosmovisor.Config, error) {
configPath, err := cmd.Flags().GetString(cosmovisor.FlagCosmovisorConfig)
if err != nil {
return nil, fmt.Errorf("failed to get config flag: %w", err)
}

cfg, err := cosmovisor.GetConfigFromFile(configPath)
if err != nil {
return nil, err
}
return cfg, nil
}

// addUpgradeCmd parses input flags and adds upgrade info to manifest
func addUpgradeCmd(cmd *cobra.Command, args []string) error {
cfg, err := getConfigFromCmd(cmd)
if err != nil {
return err
}

upgradeName, executablePath := args[0], args[1]

force, err := cmd.Flags().GetBool(cosmovisor.FlagForce)
if err != nil {
return fmt.Errorf("failed to get force flag: %w", err)
}

upgradeHeight, err := cmd.Flags().GetInt64(cosmovisor.FlagUpgradeHeight)
if err != nil {
return fmt.Errorf("failed to get upgrade-height flag: %w", err)
}

return addUpgrade(cfg, force, upgradeHeight, upgradeName, executablePath, cfg.UpgradeInfoFilePath())
}

// saveOrAbort saves data to path or aborts if file exists and force is false
func saveOrAbort(path string, data []byte, force bool) error {
if _, err := os.Stat(path); err == nil {
Expand Down
143 changes: 143 additions & 0 deletions tools/cosmovisor/cmd/cosmovisor/batch_upgrade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package main

import (
"encoding/csv"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/spf13/cobra"

"cosmossdk.io/tools/cosmovisor"
)

func NewBatchAddUpgradeCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "add-batch-upgrade [flags]",
Short: "Add multiple upgrade binaries at specified heights to cosmovisor",
Long: `This command allows you to specify multiple upgrades at once at specific heights, copying or creating a batch upgrade file that's actively watched during 'cosmovisor run'.
You can provide upgrades in two ways:
1. Using --upgrade-file: Specify a path to a headerless CSV batch upgrade file in the format:
upgrade-name,path-to-exec,upgrade-height
2. Using --upgrade-list: Provide a comma-separated list of upgrades.
Each upgrade is defined by three colon-separated values:
a. upgrade-name: A unique identifier for the upgrade
b. path-to-exec: The file path to the upgrade's executable binary
c. upgrade-height: The block height at which the upgrade should occur
This creates a batch upgrade JSON file with the upgrade-info objects in the upgrade directory.
Note: You must provide either --upgrade-file or --upgrade-list.`,
Example: `cosmovisor add-batch-upgrade --upgrade-list upgrade_v2:/path/to/v2/binary:1000000,upgrade_v3:/path/to/v3/binary:2000000
cosmovisor add-batch-upgrade --upgrade-file /path/to/batch_upgrade.csv`,
SilenceUsage: true,
Args: cobra.NoArgs,
RunE: addBatchUpgrade,
}

cmd.Flags().String("upgrade-file", "", "Path to a batch upgrade file which is a JSON array of upgrade-info objects")
cmd.Flags().StringSlice("upgrade-list", []string{}, "List of comma-separated upgrades in the format 'name:path/to/binary:height'")
cmd.MarkFlagsMutuallyExclusive("upgrade-file", "upgrade-list")

return cmd
}

// addBatchUpgrade takes in multiple specified upgrades and creates a single
// batch upgrade file out of them
func addBatchUpgrade(cmd *cobra.Command, args []string) error {
cfg, err := getConfigFromCmd(cmd)
if err != nil {
return err
}
upgradeFile, err := cmd.Flags().GetString("upgrade-file")
if err == nil && upgradeFile != "" {
return processUpgradeFile(cfg, upgradeFile)
}
upgradeList, err := cmd.Flags().GetStringSlice("upgrade-list")
if err != nil || len(upgradeList) == 0 {
return fmt.Errorf("either --upgrade-file or --upgrade-list must be provided")
}
var splitUpgrades [][]string
for _, upgrade := range upgradeList {
splitUpgrades = append(splitUpgrades, strings.Split(upgrade, ":"))
}
return processUpgradeList(cfg, splitUpgrades)
}

// processUpgradeList takes in a list of upgrades and creates a batch upgrade file
func processUpgradeList(cfg *cosmovisor.Config, upgradeList [][]string) error {
upgradeInfoPaths := []string{}
for i, upgrade := range upgradeList {
if len(upgrade) != 3 {
return fmt.Errorf("argument at position %d (%s) is invalid", i, upgrade)
}
upgradeName := filepath.Base(upgrade[0])
upgradePath := upgrade[1]
upgradeHeight, err := strconv.ParseInt(upgrade[2], 10, 64)
if err != nil {
return fmt.Errorf("upgrade height at position %d (%s) is invalid", i, upgrade[2])
}
upgradeInfoPath := cfg.UpgradeInfoFilePath() + "." + upgradeName
upgradeInfoPaths = append(upgradeInfoPaths, upgradeInfoPath)
if err := addUpgrade(cfg, true, upgradeHeight, upgradeName, upgradePath, upgradeInfoPath); err != nil {
return err
}
}

var allData []json.RawMessage
for _, uip := range upgradeInfoPaths {
fileData, err := os.ReadFile(uip)
if err != nil {
return fmt.Errorf("error reading file %s: %w", uip, err)
}

// Verify it's valid JSON
var jsonData json.RawMessage
if err := json.Unmarshal(fileData, &jsonData); err != nil {
return fmt.Errorf("error parsing JSON from file %s: %w", uip, err)
}

// Add to our slice
allData = append(allData, jsonData)
}

// Marshal the combined data
batchData, err := json.MarshalIndent(allData, "", " ")
if err != nil {
return fmt.Errorf("error marshaling combined JSON: %w", err)
}

// Write to output file
err = os.WriteFile(cfg.UpgradeInfoBatchFilePath(), batchData, 0o600)
if err != nil {
return fmt.Errorf("error writing combined JSON to file: %w", err)
}

return nil
}

// processUpgradeFile takes in a CSV batch upgrade file, parses it and calls processUpgradeList
func processUpgradeFile(cfg *cosmovisor.Config, upgradeFile string) error {
file, err := os.Open(upgradeFile)
if err != nil {
return fmt.Errorf("error opening upgrade CSV file %s: %w", upgradeFile, err)
}
defer file.Close()

r := csv.NewReader(file)
r.FieldsPerRecord = 3
r.TrimLeadingSpace = true
records, err := r.ReadAll()
if err != nil {
return fmt.Errorf("error parsing upgrade CSV file %s: %w", upgradeFile, err)
}
if err := processUpgradeList(cfg, records); err != nil {
return err
}
return nil
}
1 change: 1 addition & 0 deletions tools/cosmovisor/cmd/cosmovisor/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ func NewRootCmd() *cobra.Command {
NewVersionCmd(),
NewAddUpgradeCmd(),
NewShowUpgradeInfoCmd(),
NewBatchAddUpgradeCmd(),
NewPrepareUpgradeCmd(),
)

Expand Down
4 changes: 2 additions & 2 deletions tools/cosmovisor/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ go 1.23
require (
cosmossdk.io/log v1.4.1
cosmossdk.io/x/upgrade v0.1.4
github.com/cosmos/cosmos-sdk v0.50.7
github.com/fsnotify/fsnotify v1.7.0
github.com/otiai10/copy v1.14.0
github.com/pelletier/go-toml/v2 v2.2.3
github.com/spf13/cobra v1.8.1
Expand Down Expand Up @@ -51,7 +53,6 @@ require (
github.com/cosmos/btcutil v1.0.5 // indirect
github.com/cosmos/cosmos-db v1.0.2 // indirect
github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect
github.com/cosmos/cosmos-sdk v0.50.7 // indirect
github.com/cosmos/go-bip39 v1.0.0 // indirect
github.com/cosmos/gogogateway v1.2.0 // indirect
github.com/cosmos/gogoproto v1.7.0 // indirect
Expand All @@ -69,7 +70,6 @@ require (
github.com/emicklei/dot v1.6.2 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/getsentry/sentry-go v0.28.0 // indirect
github.com/go-kit/kit v0.13.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
Expand Down
Loading

0 comments on commit 5b53cca

Please sign in to comment.