refactor: make Cosmovisor use cobra (#11823)

## Description

Closes: #11789



---

### Author Checklist

*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*

I have...

- [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] added `!` to the type prefix if API or client breaking change
- [x] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md#pr-targeting))
- [x] provided a link to the relevant issue or specification
- [ ] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/main/docs/building-modules)
- [ ] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/main/CONTRIBUTING.md#testing)
- [x] added a changelog entry to `CHANGELOG.md`
- [x] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [x] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed

### Reviewers Checklist

*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*

I have...

- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed 
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
This commit is contained in:
Julien Robert 2022-05-02 18:25:09 +02:00 committed by GitHub
parent c0f65e1e5d
commit 8076144bfc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 174 additions and 430 deletions

View File

@ -37,7 +37,10 @@ Ref: https://keepachangelog.com/en/1.0.0/
## [Unreleased]
<!-- NOTE: when creating a new release, update cosmovisor/cmd/cosmovisor/cmd/version.go:Version -->
* [\#11731](https://github.com/cosmos/cosmos-sdk/pull/11731) `cosmovisor version --json` returns the cosmovisor version and the result of `simd --output json --long` in one JSON object.
### Features
* [\#11823](https://github.com/cosmos/cosmos-sdk/pull/11823) Refactor `cosmovisor` CLI to use `cobra`.
* [\#11731](https://github.com/cosmos/cosmos-sdk/pull/11731) `cosmovisor version -o json` returns the cosmovisor version and the result of `simd --output json --long` in one JSON object.
## v1.1.0 2022-10-02

View File

@ -37,7 +37,7 @@ To install a previous version, you can specify the version. IMPORTANT: Chains th
go install github.com/cosmos/cosmos-sdk/cosmovisor/cmd/cosmovisor@v0.1.0
```
You can run `cosmovisor --version` to check the Cosmovisor version (works only with Cosmovisor >=1.0.0).
You can run `cosmovisor version` to check the Cosmovisor version (works only with Cosmovisor >1.1.0).
You can also install from source by pulling the cosmos-sdk repository and switching to the correct version and building as follows:
@ -62,7 +62,7 @@ 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`, or `--version` - Output the `cosmovisor` version and also run the binary with the `version` argument.
* `version` - Output the `cosmovisor` version and also run the binary with the `version` argument.
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.
@ -209,7 +209,6 @@ You can also use `sha512sum` if you would prefer to use longer hashes, or `md5su
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.
### Chain Setup
Let's create a new chain using the `v0.44` version of simapp (the Cosmos SDK demo app):
@ -289,12 +288,10 @@ cp ./build/simd $DAEMON_HOME/cosmovisor/genesis/bin
Now you can run cosmovisor with simapp v0.44:
```sh
cosmovisor run start
```
#### Update App
Update app to the latest version (e.g. v0.45).
@ -314,7 +311,6 @@ mkdir -p $DAEMON_HOME/cosmovisor/upgrades/test1/bin
cp ./build/simd $DAEMON_HOME/cosmovisor/upgrades/test1/bin
```
Open a new terminal window and submit an upgrade proposal along with a deposit and a vote (these commands must be run within 20 seconds of each other):
```sh

View File

@ -1,49 +0,0 @@
package cmd
import (
"fmt"
"github.com/cosmos/cosmos-sdk/cosmovisor"
)
// HelpArgs are the strings that indicate a cosmovisor help command.
var HelpArgs = []string{"help", "--help", "-h"}
// ShouldGiveHelp checks the env and provided args to see if help is needed or being requested.
// Help is needed if either cosmovisor.EnvName and/or cosmovisor.EnvHome env vars aren't set.
// Help is requested if the first arg is "help", "--help", or "-h".
func ShouldGiveHelp(arg string) bool {
return isOneOf(arg, HelpArgs)
}
// DoHelp outputs help text
func DoHelp() error {
// Not using the logger for this output because the header and footer look weird for help text.
fmt.Println(GetHelpText())
return nil
}
// GetHelpText creates the help text multi-line string.
func GetHelpText() string {
return fmt.Sprintf(`Cosmosvisor - A process manager for Cosmos SDK application binaries.
Cosmovisor is a wrapper for a Cosmos SDK based App (set using the required %s env variable).
It starts the App by passing all provided arguments and monitors the %s/data/upgrade-info.json
file to perform an update. The upgrade-info.json file is created by the App x/upgrade module
when the blockchain height reaches an approved upgrade proposal. The file includes data from
the proposal. Cosmovisor interprets that data to perform an update: switch a current binary
and restart the App.
Configuration of Cosmovisor is done through environment variables, which are
documented in: https://github.com/cosmos/cosmos-sdk/tree/main/cosmovisor/README.md
To get help for the configured binary:
cosmovisor run help
Available Commands:
help This help message
run Runs app passing all subsequent parameters
version Prints version of cosmovisor and the associated app.
`, cosmovisor.EnvName, cosmovisor.EnvHome)
}

View File

@ -1,44 +0,0 @@
package cmd
import (
"strings"
"github.com/rs/zerolog"
)
// RunCosmovisorCommand executes the desired cosmovisor command.
func RunCosmovisorCommand(logger *zerolog.Logger, args []string) error {
arg0 := ""
if len(args) > 0 {
arg0 = strings.TrimSpace(args[0])
}
switch {
case IsVersionCommand(arg0):
return PrintVersion(logger, args[1:])
case ShouldGiveHelp(arg0):
return DoHelp()
case IsRunCommand(arg0):
return Run(logger, args[1:])
}
warnRun := func() {
logger.Warn().Msg("use of cosmovisor without the 'run' command is deprecated. Use: cosmovisor run [args]")
}
warnRun()
defer warnRun()
return Run(logger, args)
}
// isOneOf returns true if the given arg equals one of the provided options (ignoring case).
func isOneOf(arg string, options []string) bool {
for _, opt := range options {
if strings.EqualFold(arg, opt) {
return true
}
}
return false
}

View File

@ -1,76 +0,0 @@
package cmd
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestIsRunCommand(t *testing.T) {
cases := []struct {
name string
arg string
expected bool
}{
{
name: "empty string",
arg: "",
expected: false,
},
{
name: "random",
arg: "random",
expected: false,
},
{
name: "run",
arg: "run",
expected: true,
},
{
name: "run weird casing",
arg: "RUn",
expected: true,
},
{
name: "--run",
arg: "--run",
expected: false,
},
{
name: "help",
arg: "help",
expected: false,
},
{
name: "-h",
arg: "-h",
expected: false,
},
{
name: "--help",
arg: "--help",
expected: false,
},
{
name: "version",
arg: "version",
expected: false,
},
{
name: "--version",
arg: "--version",
expected: false,
},
}
for _, tc := range cases {
t.Run(fmt.Sprintf("%s - %t", tc.name, tc.expected), func(t *testing.T) {
actual := IsRunCommand(tc.arg)
require.Equal(t, tc.expected, actual)
})
}
}
// TODO: Write tests for func Run(args []string) error

View File

@ -1,88 +0,0 @@
package cmd
import (
"encoding/json"
"fmt"
"os"
"strings"
"time"
cverrors "github.com/cosmos/cosmos-sdk/cosmovisor/errors"
"github.com/rs/zerolog"
)
var (
// FlagJSON formats the output in json
FlagJSON = "--json"
// Version represents Cosmovisor version value. Overwritten during build
Version = "1.1.0"
// VersionArgs is the strings that indicate a cosmovisor version command.
VersionArgs = []string{"version", "--version"}
)
// IsVersionCommand checks if the given args indicate that the version is being requested.
func IsVersionCommand(arg string) bool {
return isOneOf(arg, VersionArgs)
}
// PrintVersion prints the cosmovisor version.
func PrintVersion(logger *zerolog.Logger, args []string) error {
for _, arg := range args {
if strings.Contains(arg, FlagJSON) {
return printVersionJSON(logger, args)
}
}
return printVersion(logger, args)
}
func printVersion(logger *zerolog.Logger, args []string) error {
fmt.Println("Cosmovisor Version: ", Version)
if err := Run(logger, append([]string{"version"}, args...)); err != nil {
handleRunVersionFailure(err)
}
return nil
}
func printVersionJSON(logger *zerolog.Logger, args []string) error {
buf := new(strings.Builder)
// disable logger
l := logger.Level(zerolog.Disabled)
logger = &l
if err := Run(
logger,
[]string{"version", "--long", "--output", "json"},
StdOutRunOption(buf),
); err != nil {
handleRunVersionFailure(err)
}
out, err := json.Marshal(struct {
Version string `json:"cosmovisor_version"`
AppVersion json.RawMessage `json:"app_version"`
}{
Version: Version,
AppVersion: json.RawMessage(buf.String()),
})
if err != nil {
l := logger.Level(zerolog.TraceLevel)
logger = &l
return fmt.Errorf("Can't print version output, expected valid json from APP, got: %s - %w", buf.String(), err)
}
fmt.Println(string(out))
return nil
}
func handleRunVersionFailure(err error) {
// Check the config and output details or any errors.
// Not using the cosmovisor.Logger in order to ignore any level it might have set,
// and also to not have any of the extra parameters in the output.
output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.Kitchen}
logger := zerolog.New(output).With().Timestamp().Logger()
cverrors.LogErrors(&logger, "Can't run APP version", err)
}

View File

@ -1,85 +0,0 @@
package cmd
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestIsVersionCommand(t *testing.T) {
cases := []struct {
name string
arg string
expected bool
}{
{
name: "empty string",
arg: "",
expected: false,
},
{
name: "random",
arg: "random",
expected: false,
},
{
name: "version",
arg: "version",
expected: true,
},
{
name: "--version",
arg: "--version",
expected: true,
},
{
name: "version weird casing",
arg: "veRSiOn",
expected: true,
},
{
// -v should be reserved for verbose, and should not be used for --version.
name: "-v",
arg: "-v",
expected: false,
},
{
name: "typo",
arg: "vrsion",
expected: false,
},
{
name: "non version command",
arg: "start",
expected: false,
},
{
name: "help",
arg: "help",
expected: false,
},
{
name: "-h",
arg: "-h",
expected: false,
},
{
name: "--help",
arg: "--help",
expected: false,
},
{
name: "run",
arg: "run",
expected: false,
},
}
for _, tc := range cases {
t.Run(fmt.Sprintf("%s - %t", tc.name, tc.expected), func(t *testing.T) {
actual := IsVersionCommand(tc.arg)
require.Equal(t, tc.expected, actual)
})
}
}

View File

@ -0,0 +1,24 @@
package main
import (
"fmt"
"github.com/cosmos/cosmos-sdk/cosmovisor"
)
// GetHelpText creates the help text multi-line string.
func GetHelpText() string {
return fmt.Sprintf(`Cosmosvisor - A process manager for Cosmos SDK application binaries.
Cosmovisor is a wrapper for a Cosmos SDK based App (set using the required %s env variable).
It starts the App by passing all provided arguments and monitors the %s/data/upgrade-info.json
file to perform an update. The upgrade-info.json file is created by the App x/upgrade module
when the blockchain height reaches an approved upgrade proposal. The file includes data from
the proposal. Cosmovisor interprets that data to perform an update: switch a current binary
and restart the App.
Configuration of Cosmovisor is done through environment variables, which are
documented in: https://github.com/cosmos/cosmos-sdk/tree/main/cosmovisor/README.md`,
cosmovisor.EnvName, cosmovisor.EnvHome,
)
}

View File

@ -1,11 +1,10 @@
package cmd
package main
import (
"fmt"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
@ -88,72 +87,6 @@ func (s *HelpTestSuite) setEnv(t *testing.T, env *cosmovisorHelpEnv) {
}
}
func (s HelpTestSuite) TestShouldGiveHelpArg() {
initialEnv := s.clearEnv()
defer s.setEnv(nil, initialEnv)
s.setEnv(s.T(), &cosmovisorHelpEnv{"/testhome", "testname"})
tests := []struct {
name string
arg string
expected bool
}{
{
name: "empty string",
arg: "",
expected: false,
},
{
name: "random",
arg: "random",
expected: false,
},
{
name: "help",
arg: "help",
expected: true,
},
{
name: "-h",
arg: "-h",
expected: true,
},
{
name: "--help",
arg: "--help",
expected: true,
},
{
name: "help weird casing",
arg: "hELP",
expected: true,
},
{
name: "version",
arg: "version",
expected: false,
},
{
name: "--version",
arg: "--version",
expected: false,
},
{
name: "run",
arg: "run",
expected: false,
},
}
for _, tc := range tests {
s.T().Run(fmt.Sprintf("%s - %t", tc.name, tc.expected), func(t *testing.T) {
actual := ShouldGiveHelp(tc.arg)
assert.Equal(t, tc.expected, actual)
})
}
}
func (s *HelpTestSuite) TestGetHelpText() {
expectedPieces := []string{
"Cosmosvisor",

View File

@ -1,16 +1,18 @@
package main
import (
"context"
"os"
"github.com/cosmos/cosmos-sdk/cosmovisor"
"github.com/cosmos/cosmos-sdk/cosmovisor/cmd/cosmovisor/cmd"
cverrors "github.com/cosmos/cosmos-sdk/cosmovisor/errors"
)
func main() {
logger := cosmovisor.NewLogger()
if err := cmd.RunCosmovisorCommand(logger, os.Args[1:]); err != nil {
ctx := context.WithValue(context.Background(), cosmovisor.LoggerKey, logger)
if err := rootCmd.ExecuteContext(ctx); err != nil {
cverrors.LogErrors(logger, "", err)
os.Exit(1)
}

View File

@ -0,0 +1,11 @@
package main
import (
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "cosmovisor",
Short: "A process manager for Cosmos SDK application binaries.",
Long: GetHelpText(),
}

View File

@ -1,16 +1,25 @@
package cmd
package main
import (
"github.com/cosmos/cosmos-sdk/cosmovisor"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
)
// RunArgs are the strings that indicate a cosmovisor run command.
var RunArgs = []string{"run"}
func init() {
rootCmd.AddCommand(runCmd)
}
// IsRunCommand checks if the given args indicate that a run is desired.
func IsRunCommand(arg string) bool {
return isOneOf(arg, RunArgs)
var runCmd = &cobra.Command{
Use: "run",
Short: "Run an APP command.",
SilenceUsage: true,
DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, args []string) error {
logger := cmd.Context().Value(cosmovisor.LoggerKey).(*zerolog.Logger)
return Run(logger, args)
},
}
// Run runs the configured program with the given args and monitors it for upgrades.

View File

@ -1,4 +1,4 @@
package cmd
package main
import (
"io"

View File

@ -0,0 +1,4 @@
package main
// TODO: Write tests for func Run(args []string) error
// https://github.com/cosmos/cosmos-sdk/issues/11852

View File

@ -0,0 +1,81 @@
package main
import (
"encoding/json"
"fmt"
"strings"
"github.com/cosmos/cosmos-sdk/cosmovisor"
"github.com/rs/zerolog"
"github.com/spf13/cobra"
tmcli "github.com/tendermint/tendermint/libs/cli"
)
func init() {
versionCmd.Flags().StringP(OutputFlag, "o", "text", "Output format (text|json)")
rootCmd.AddCommand(versionCmd)
}
var (
// Version represents Cosmovisor version value. Overwritten during build
Version = "1.1.0"
// OutputFlag defines the output format flag
OutputFlag = tmcli.OutputFlag
)
var versionCmd = &cobra.Command{
Use: "version",
Short: "Prints the version of Cosmovisor.",
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
logger := cmd.Context().Value(cosmovisor.LoggerKey).(*zerolog.Logger)
if val, err := cmd.Flags().GetString(OutputFlag); val == "json" && err == nil {
return printVersionJSON(logger, args)
}
return printVersion(logger, args)
},
}
func printVersion(logger *zerolog.Logger, args []string) error {
fmt.Println("cosmovisor version: ", Version)
if err := Run(logger, append([]string{"version"}, args...)); err != nil {
return fmt.Errorf("failed to run version command: %w", err)
}
return nil
}
func printVersionJSON(logger *zerolog.Logger, args []string) error {
buf := new(strings.Builder)
// disable logger
l := logger.Level(zerolog.Disabled)
logger = &l
if err := Run(
logger,
[]string{"version", "--long", "--output", "json"},
StdOutRunOption(buf),
); err != nil {
return fmt.Errorf("failed to run version command: %w", err)
}
out, err := json.Marshal(struct {
Version string `json:"cosmovisor_version"`
AppVersion json.RawMessage `json:"app_version"`
}{
Version: Version,
AppVersion: json.RawMessage(buf.String()),
})
if err != nil {
l := logger.Level(zerolog.TraceLevel)
logger = &l
return fmt.Errorf("can't print version output, expected valid json from APP, got: %s - %w", buf.String(), err)
}
fmt.Println(string(out))
return nil
}

View File

@ -0,0 +1,21 @@
package main
import (
"context"
"testing"
"github.com/cosmos/cosmos-sdk/cosmovisor"
"github.com/cosmos/cosmos-sdk/testutil"
"github.com/stretchr/testify/require"
)
func TestVersionCommand_Error(t *testing.T) {
logger := cosmovisor.NewLogger()
rootCmd.SetArgs([]string{"version"})
_, out := testutil.ApplyMockIO(rootCmd)
ctx := context.WithValue(context.Background(), cosmovisor.LoggerKey, logger)
require.Error(t, rootCmd.ExecuteContext(ctx))
require.Contains(t, out.String(), "DAEMON_NAME is not set")
}

View File

@ -7,7 +7,9 @@ require (
github.com/hashicorp/go-getter v1.5.11
github.com/otiai10/copy v1.7.0
github.com/rs/zerolog v1.26.1
github.com/spf13/cobra v1.4.0
github.com/stretchr/testify v1.7.1
github.com/tendermint/tendermint v0.35.2
)
require (
@ -85,7 +87,6 @@ require (
github.com/sasha-s/go-deadlock v0.2.1-0.20190427202633-1595213edefa // indirect
github.com/spf13/afero v1.8.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/cobra v1.4.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.10.1 // indirect
@ -95,7 +96,6 @@ require (
github.com/tendermint/btcd v0.1.1 // indirect
github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15 // indirect
github.com/tendermint/go-amino v0.16.0 // indirect
github.com/tendermint/tendermint v0.35.2 // indirect
github.com/tendermint/tm-db v0.6.6 // indirect
github.com/ulikunitz/xz v0.5.8 // indirect
github.com/zondax/hid v0.9.1-0.20220302062450-5552068d2266 // indirect

View File

@ -7,6 +7,8 @@ import (
"github.com/rs/zerolog"
)
var LoggerKey struct{}
func NewLogger() *zerolog.Logger {
output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.Kitchen}
logger := zerolog.New(output).With().Str("module", "cosmovisor").Timestamp().Logger()