Add client config subcommand to CLI (#8953)

* add client config

* addressed reviewers comments

* refactored,ready for review

* fixed linter issues and addressed reviewers comments

* Bump golangci-lint

* fix linter warnings

* fix some tests

Co-authored-by: Alessio Treglia <alessio@tendermint.com>
Co-authored-by: Amaury <1293565+amaurym@users.noreply.github.com>
This commit is contained in:
Andrei Ivasko 2021-03-31 03:04:33 -07:00 committed by GitHub
parent 413938c5ed
commit 410d8edb38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 332 additions and 21 deletions

View File

@ -23,7 +23,7 @@ jobs:
- uses: golangci/golangci-lint-action@master
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.37
version: v1.39
args: --timeout 10m
github-token: ${{ secrets.github_token }}
if: env.GIT_DIFF

View File

@ -8,7 +8,6 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/tendermint/tendermint/libs/cli"
rpchttp "github.com/tendermint/tendermint/rpc/client/http"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
@ -94,11 +93,6 @@ func ReadPersistentCommandFlags(clientCtx Context, flagSet *pflag.FlagSet) (Cont
clientCtx = clientCtx.WithOutputFormat(output)
}
if clientCtx.HomeDir == "" || flagSet.Changed(flags.FlagHome) {
homeDir, _ := flagSet.GetString(flags.FlagHome)
clientCtx = clientCtx.WithHomeDir(homeDir)
}
if !clientCtx.Simulate || flagSet.Changed(flags.FlagDryRun) {
dryRun, _ := flagSet.GetBool(flags.FlagDryRun)
clientCtx = clientCtx.WithSimulation(dryRun)
@ -125,7 +119,7 @@ func ReadPersistentCommandFlags(clientCtx Context, flagSet *pflag.FlagSet) (Cont
keyringBackend, _ := flagSet.GetString(flags.FlagKeyringBackend)
if keyringBackend != "" {
kr, err := newKeyringFromFlags(clientCtx, keyringBackend)
kr, err := NewKeyringFromBackend(clientCtx, keyringBackend)
if err != nil {
return clientCtx, err
}
@ -139,7 +133,7 @@ func ReadPersistentCommandFlags(clientCtx Context, flagSet *pflag.FlagSet) (Cont
if rpcURI != "" {
clientCtx = clientCtx.WithNodeURI(rpcURI)
client, err := rpchttp.New(rpcURI, "/websocket")
client, err := NewClientFromNode(rpcURI)
if err != nil {
return clientCtx, err
}
@ -255,6 +249,21 @@ func readTxCommandFlags(clientCtx Context, flagSet *pflag.FlagSet) (Context, err
return clientCtx, nil
}
// ReadHomeFlag checks if home flag is changed.
// If this is a case, we update HomeDir field of Client Context
/* Discovered a bug with Cory
./build/simd init andrei --home ./test
cd test/config there is no client.toml configuration file
*/
func ReadHomeFlag(clientCtx Context, cmd *cobra.Command) Context {
if cmd.Flags().Changed(flags.FlagHome) {
rootDir, _ := cmd.Flags().GetString(flags.FlagHome)
clientCtx = clientCtx.WithHomeDir(rootDir)
}
return clientCtx
}
// GetClientQueryContext returns a Context from a command with fields set based on flags
// defined in AddQueryFlagsToCmd. An error is returned if any flag query fails.
//

96
client/config/cmd.go Normal file
View File

@ -0,0 +1,96 @@
package config
import (
"encoding/json"
"fmt"
"path/filepath"
tmcli "github.com/tendermint/tendermint/libs/cli"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
)
// Cmd returns a CLI command to interactively create an application CLI
// config file.
func Cmd() *cobra.Command {
cmd := &cobra.Command{
Use: "config <key> [value]",
Short: "Create or query an application CLI configuration file",
RunE: runConfigCmd,
Args: cobra.RangeArgs(0, 2),
}
return cmd
}
func runConfigCmd(cmd *cobra.Command, args []string) error {
clientCtx := client.GetClientContextFromCmd(cmd)
configPath := filepath.Join(clientCtx.HomeDir, "config")
conf, err := getClientConfig(configPath, clientCtx.Viper)
if err != nil {
return fmt.Errorf("couldn't get client config: %v", err)
}
switch len(args) {
case 0:
// print all client config fields to sdt out
s, _ := json.MarshalIndent(conf, "", "\t")
cmd.Println(string(s))
case 1:
// it's a get
key := args[0]
switch key {
case flags.FlagChainID:
cmd.Println(conf.ChainID)
case flags.FlagKeyringBackend:
cmd.Println(conf.KeyringBackend)
case tmcli.OutputFlag:
cmd.Println(conf.Output)
case flags.FlagNode:
cmd.Println(conf.Node)
case flags.FlagBroadcastMode:
cmd.Println(conf.BroadcastMode)
default:
err := errUnknownConfigKey(key)
return fmt.Errorf("couldn't get the value for the key: %v, error: %v", key, err)
}
case 2:
// it's set
key, value := args[0], args[1]
switch key {
case flags.FlagChainID:
conf.SetChainID(value)
case flags.FlagKeyringBackend:
conf.SetKeyringBackend(value)
case tmcli.OutputFlag:
conf.SetOutput(value)
case flags.FlagNode:
conf.SetNode(value)
case flags.FlagBroadcastMode:
conf.SetBroadcastMode(value)
default:
return errUnknownConfigKey(key)
}
confFile := filepath.Join(configPath, "client.toml")
if err := writeConfigToFile(confFile, conf); err != nil {
return fmt.Errorf("could not write client config to the file: %v", err)
}
default:
panic("cound not execute config command")
}
return nil
}
func errUnknownConfigKey(key string) error {
return fmt.Errorf("unknown configuration key: %q", key)
}

96
client/config/config.go Normal file
View File

@ -0,0 +1,96 @@
package config
import (
"fmt"
"os"
"path/filepath"
"github.com/cosmos/cosmos-sdk/client"
)
// Default constants
const (
chainID = ""
keyringBackend = "os"
output = "text"
node = "tcp://localhost:26657"
broadcastMode = "sync"
)
type ClientConfig struct {
ChainID string `mapstructure:"chain-id" json:"chain-id"`
KeyringBackend string `mapstructure:"keyring-backend" json:"keyring-backend"`
Output string `mapstructure:"output" json:"output"`
Node string `mapstructure:"node" json:"node"`
BroadcastMode string `mapstructure:"broadcast-mode" json:"broadcast-mode"`
}
// defaultClientConfig returns the reference to ClientConfig with default values.
func defaultClientConfig() *ClientConfig {
return &ClientConfig{chainID, keyringBackend, output, node, broadcastMode}
}
func (c *ClientConfig) SetChainID(chainID string) {
c.ChainID = chainID
}
func (c *ClientConfig) SetKeyringBackend(keyringBackend string) {
c.KeyringBackend = keyringBackend
}
func (c *ClientConfig) SetOutput(output string) {
c.Output = output
}
func (c *ClientConfig) SetNode(node string) {
c.Node = node
}
func (c *ClientConfig) SetBroadcastMode(broadcastMode string) {
c.BroadcastMode = broadcastMode
}
// ReadFromClientConfig reads values from client.toml file and updates them in client Context
func ReadFromClientConfig(ctx client.Context) (client.Context, error) {
configPath := filepath.Join(ctx.HomeDir, "config")
configFilePath := filepath.Join(configPath, "client.toml")
conf := defaultClientConfig()
// if config.toml file does not exist we create it and write default ClientConfig values into it.
if _, err := os.Stat(configFilePath); os.IsNotExist(err) {
if err := ensureConfigPath(configPath); err != nil {
return ctx, fmt.Errorf("couldn't make client config: %v", err)
}
if err := writeConfigToFile(configFilePath, conf); err != nil {
return ctx, fmt.Errorf("could not write client config to the file: %v", err)
}
}
conf, err := getClientConfig(configPath, ctx.Viper)
if err != nil {
return ctx, fmt.Errorf("couldn't get client config: %v", err)
}
// we need to update KeyringDir field on Client Context first cause it is used in NewKeyringFromBackend
ctx = ctx.WithOutputFormat(conf.Output).
WithChainID(conf.ChainID)
keyring, err := client.NewKeyringFromBackend(ctx, conf.KeyringBackend)
if err != nil {
return ctx, fmt.Errorf("couldn't get key ring: %v", err)
}
ctx = ctx.WithKeyring(keyring)
// https://github.com/cosmos/cosmos-sdk/issues/8986
client, err := client.NewClientFromNode(conf.Node)
if err != nil {
return ctx, fmt.Errorf("couldn't get client from nodeURI: %v", err)
}
ctx = ctx.WithNodeURI(conf.Node).
WithClient(client).
WithBroadcastMode(conf.BroadcastMode)
return ctx, nil
}

70
client/config/toml.go Normal file
View File

@ -0,0 +1,70 @@
package config
import (
"bytes"
"io/ioutil"
"os"
"text/template"
"github.com/spf13/viper"
)
const defaultConfigTemplate = `# This is a TOML config file.
# For more information, see https://github.com/toml-lang/toml
###############################################################################
### Client Configuration ###
###############################################################################
# The network chain ID
chain-id = "{{ .ChainID }}"
# The keyring's backend, where the keys are stored (os|file|kwallet|pass|test|memory)
keyring-backend = "{{ .KeyringBackend }}"
# CLI output format (text|json)
output = "{{ .Output }}"
# <host>:<port> to Tendermint RPC interface for this chain
node = "{{ .Node }}"
# Transaction broadcasting mode (sync|async|block)
broadcast-mode = "{{ .BroadcastMode }}"
`
// writeConfigToFile parses defaultConfigTemplate, renders config using the template and writes it to
// configFilePath.
func writeConfigToFile(configFilePath string, config *ClientConfig) error {
var buffer bytes.Buffer
tmpl := template.New("clientConfigFileTemplate")
configTemplate, err := tmpl.Parse(defaultConfigTemplate)
if err != nil {
return err
}
if err := configTemplate.Execute(&buffer, config); err != nil {
return err
}
return ioutil.WriteFile(configFilePath, buffer.Bytes(), 0600)
}
// ensureConfigPath creates a directory configPath if it does not exist
func ensureConfigPath(configPath string) error {
return os.MkdirAll(configPath, os.ModePerm)
}
// getClientConfig reads values from client.toml file and unmarshalls them into ClientConfig
func getClientConfig(configPath string, v *viper.Viper) (*ClientConfig, error) {
v.AddConfigPath(configPath)
v.SetConfigName("client")
v.SetConfigType("toml")
if err := v.ReadInConfig(); err != nil {
return nil, err
}
conf := new(ClientConfig)
if err := v.Unmarshal(conf); err != nil {
return nil, err
}
return conf, nil
}

View File

@ -5,6 +5,8 @@ import (
"io"
"os"
"github.com/spf13/viper"
"gopkg.in/yaml.v2"
"github.com/gogo/protobuf/proto"
@ -46,6 +48,7 @@ type Context struct {
AccountRetriever AccountRetriever
NodeURI string
FeeGranter sdk.AccAddress
Viper *viper.Viper
// TODO: Deprecated (remove).
LegacyAmino *codec.LegacyAmino
@ -133,7 +136,9 @@ func (ctx Context) WithChainID(chainID string) Context {
// WithHomeDir returns a copy of the Context with HomeDir set.
func (ctx Context) WithHomeDir(dir string) Context {
ctx.HomeDir = dir
if dir != "" {
ctx.HomeDir = dir
}
return ctx
}
@ -220,6 +225,14 @@ func (ctx Context) WithInterfaceRegistry(interfaceRegistry codectypes.InterfaceR
return ctx
}
// WithViper returns the context with Viper field. This Viper instance is used to read
// client-side config from the config file.
func (ctx Context) WithViper() Context {
v := viper.New()
ctx.Viper = v
return ctx
}
// PrintString prints the raw string to ctx.Output if it's defined, otherwise to os.Stdout
func (ctx Context) PrintString(str string) error {
return ctx.PrintBytes([]byte(str))
@ -330,7 +343,8 @@ func GetFromFields(kr keyring.Keyring, from string, genOnly bool) (sdk.AccAddres
return info.GetAddress(), info.GetName(), info.GetType(), nil
}
func newKeyringFromFlags(ctx Context, backend string) (keyring.Keyring, error) {
// NewKeyringFromBackend gets a Keyring object from a backend
func NewKeyringFromBackend(ctx Context, backend string) (keyring.Keyring, error) {
if ctx.GenerateOnly || ctx.Simulate {
return keyring.New(sdk.KeyringServiceName(), keyring.BackendMemory, ctx.KeyringDir, ctx.Input, ctx.KeyringOptions...)
}

View File

@ -33,6 +33,7 @@ func Test_runDeleteCmd(t *testing.T) {
path := sdk.GetConfig().GetFullBIP44Path()
cmd.SetArgs([]string{"blah", fmt.Sprintf("--%s=%s", flags.FlagHome, kbHome)})
kb, err := keyring.New(sdk.KeyringServiceName(), keyring.BackendTest, kbHome, mockIn)
require.NoError(t, err)
@ -42,9 +43,10 @@ func Test_runDeleteCmd(t *testing.T) {
_, _, err = kb.NewMnemonic(fakeKeyName2, keyring.English, sdk.FullFundraiserPath, keyring.DefaultBIP39Passphrase, hd.Secp256k1)
require.NoError(t, err)
cmd.SetArgs([]string{"blah", fmt.Sprintf("--%s=%s", flags.FlagHome, kbHome)})
clientCtx := client.Context{}.
WithKeyringDir(kbHome).
WithKeyring(kb)
clientCtx := client.Context{}.WithKeyring(kb)
ctx := context.WithValue(context.Background(), client.ClientContextKey, &clientCtx)
err = cmd.ExecuteContext(ctx)

View File

@ -45,7 +45,9 @@ func Test_runExportCmd(t *testing.T) {
mockIn.Reset("123456789\n123456789\n")
cmd.SetArgs(args)
clientCtx := client.Context{}.WithKeyring(kb)
clientCtx := client.Context{}.
WithKeyringDir(kbHome).
WithKeyring(kb)
ctx := context.WithValue(context.Background(), client.ClientContextKey, &clientCtx)
require.NoError(t, cmd.ExecuteContext(ctx))

View File

@ -25,7 +25,9 @@ func Test_runImportCmd(t *testing.T) {
kbHome := t.TempDir()
kb, err := keyring.New(sdk.KeyringServiceName(), keyring.BackendTest, kbHome, mockIn)
clientCtx := client.Context{}.WithKeyring(kb)
clientCtx := client.Context{}.
WithKeyringDir(kbHome).
WithKeyring(kb)
ctx := context.WithValue(context.Background(), client.ClientContextKey, &clientCtx)
require.NoError(t, err)

View File

@ -2,7 +2,6 @@ package keys
import (
"github.com/spf13/cobra"
"github.com/tendermint/tendermint/libs/cli"
"github.com/cosmos/cosmos-sdk/client"
)
@ -37,8 +36,7 @@ func runListCmd(cmd *cobra.Command, _ []string) error {
cmd.SetOut(cmd.OutOrStdout())
if ok, _ := cmd.Flags().GetBool(flagListNames); !ok {
output, _ := cmd.Flags().GetString(cli.OutputFlag)
printInfos(cmd.OutOrStdout(), infos, output)
printInfos(cmd.OutOrStdout(), infos, clientCtx.OutputFormat)
return nil
}

View File

@ -48,7 +48,9 @@ func Test_runShowCmd(t *testing.T) {
kb, err := keyring.New(sdk.KeyringServiceName(), keyring.BackendTest, kbHome, mockIn)
require.NoError(t, err)
clientCtx := client.Context{}.WithKeyring(kb)
clientCtx := client.Context{}.
WithKeyringDir(kbHome).
WithKeyring(kb)
ctx := context.WithValue(context.Background(), client.ClientContextKey, &clientCtx)
cmd.SetArgs([]string{"invalid"})

View File

@ -2,6 +2,7 @@ package client
import (
"github.com/spf13/pflag"
rpchttp "github.com/tendermint/tendermint/rpc/client/http"
"github.com/cosmos/cosmos-sdk/client/flags"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
@ -69,3 +70,11 @@ func ReadPageRequest(flagSet *pflag.FlagSet) (*query.PageRequest, error) {
Reverse: reverse,
}, nil
}
// NewClientFromNode sets up Client implementation that communicates with a Tendermint node over
// JSON RPC and WebSockets
// TODO: We might not need to manually append `/websocket`:
// https://github.com/cosmos/cosmos-sdk/issues/8986
func NewClientFromNode(nodeURI string) (*rpchttp.HTTP, error) {
return rpchttp.New(nodeURI, "/websocket")
}

View File

@ -312,8 +312,8 @@ func (c converter) Tx(rawTx tmtypes.Tx, txResult *abci.ResponseDeliverTx) (*rose
}
// get operations from msgs
msgs := tx.GetMsgs()
var rawTxOps []*rosettatypes.Operation
for _, msg := range msgs {
ops, err := c.Ops(status, msg)
if err != nil {

View File

@ -14,6 +14,7 @@ import (
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/client"
config "github.com/cosmos/cosmos-sdk/client/config"
"github.com/cosmos/cosmos-sdk/client/debug"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/keys"
@ -45,12 +46,20 @@ func NewRootCmd() (*cobra.Command, params.EncodingConfig) {
WithInput(os.Stdin).
WithAccountRetriever(types.AccountRetriever{}).
WithBroadcastMode(flags.BroadcastBlock).
WithHomeDir(simapp.DefaultNodeHome)
WithHomeDir(simapp.DefaultNodeHome).
WithViper()
rootCmd := &cobra.Command{
Use: "simd",
Short: "simulation app",
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
initClientCtx = client.ReadHomeFlag(initClientCtx, cmd)
initClientCtx, err := config.ReadFromClientConfig(initClientCtx)
if err != nil {
return err
}
if err := client.SetCmdClientContextHandler(initClientCtx, cmd); err != nil {
return err
}
@ -75,6 +84,7 @@ func initRootCmd(rootCmd *cobra.Command, encodingConfig params.EncodingConfig) {
tmcli.NewCompletionCmd(rootCmd, true),
testnetCmd(simapp.ModuleBasics, banktypes.GenesisBalancesIterator{}),
debug.Cmd(),
config.Cmd(),
)
a := appCreator{encodingConfig}

View File

@ -341,6 +341,7 @@ func New(t *testing.T, cfg Config) *Network {
srvconfig.WriteConfigFile(filepath.Join(nodeDir, "config/app.toml"), appCfg)
clientCtx := client.Context{}.
WithKeyringDir(clientDir).
WithKeyring(kb).
WithHomeDir(tmCfg.RootDir).
WithChainID(cfg.ChainID).