From be34d525ba42c607ea866ffe95dc8910888ab03b Mon Sep 17 00:00:00 2001 From: Amaury <1293565+amaurym@users.noreply.github.com> Date: Mon, 17 May 2021 17:07:50 +0200 Subject: [PATCH] Add client config subcommand to CLI (backport #8953) (#9255) * 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 Co-authored-by: Amaury <1293565+amaurym@users.noreply.github.com> * Fix build Co-authored-by: Andrei Ivasko Co-authored-by: Alessio Treglia --- .github/workflows/lint.yml | 2 +- client/cmd.go | 24 ++++++++-- client/config/cmd.go | 96 +++++++++++++++++++++++++++++++++++++ client/config/config.go | 96 +++++++++++++++++++++++++++++++++++++ client/config/toml.go | 70 +++++++++++++++++++++++++++ client/context.go | 20 ++++++-- client/keys/delete_test.go | 6 ++- client/keys/export_test.go | 4 +- client/keys/import_test.go | 4 +- client/keys/list.go | 4 +- client/keys/show_test.go | 4 +- client/utils.go | 9 ++++ simapp/simd/cmd/root.go | 12 ++++- testutil/network/network.go | 1 + 14 files changed, 336 insertions(+), 16 deletions(-) create mode 100644 client/config/cmd.go create mode 100644 client/config/config.go create mode 100644 client/config/toml.go diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 157f0d217850..e49fa0bce8c4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -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.28 + version: v1.39 args: --timeout 10m github-token: ${{ secrets.github_token }} if: env.GIT_DIFF diff --git a/client/cmd.go b/client/cmd.go index eccf59c3515c..f27aea3691f4 100644 --- a/client/cmd.go +++ b/client/cmd.go @@ -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" @@ -98,6 +97,10 @@ func ReadPersistentCommandFlags(clientCtx Context, flagSet *pflag.FlagSet) (Cont 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) + } if clientCtx.KeyringDir == "" || flagSet.Changed(flags.FlagKeyringDir) { keyringDir, _ := flagSet.GetString(flags.FlagKeyringDir) @@ -120,7 +123,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 } @@ -134,7 +137,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 } @@ -242,6 +245,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. // diff --git a/client/config/cmd.go b/client/config/cmd.go new file mode 100644 index 000000000000..523939f39ed0 --- /dev/null +++ b/client/config/cmd.go @@ -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 [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) +} diff --git a/client/config/config.go b/client/config/config.go new file mode 100644 index 000000000000..391a0bf4a620 --- /dev/null +++ b/client/config/config.go @@ -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 +} diff --git a/client/config/toml.go b/client/config/toml.go new file mode 100644 index 000000000000..0393a5b6acc8 --- /dev/null +++ b/client/config/toml.go @@ -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 }}" +# : 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 +} diff --git a/client/context.go b/client/context.go index 0e3d1181af6e..f94e13a9583c 100644 --- a/client/context.go +++ b/client/context.go @@ -5,6 +5,8 @@ import ( "io" "os" + "github.com/spf13/viper" + "gopkg.in/yaml.v2" "github.com/gogo/protobuf/proto" @@ -44,6 +46,7 @@ type Context struct { TxConfig TxConfig AccountRetriever AccountRetriever NodeURI string + Viper *viper.Viper // TODO: Deprecated (remove). LegacyAmino *codec.LegacyAmino @@ -125,7 +128,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 } @@ -205,6 +210,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)) @@ -315,8 +328,9 @@ 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) { - if ctx.GenerateOnly { +// 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) } diff --git a/client/keys/delete_test.go b/client/keys/delete_test.go index 5a0013967196..af2c926158dd 100644 --- a/client/keys/delete_test.go +++ b/client/keys/delete_test.go @@ -33,6 +33,7 @@ func Test_runDeleteCmd(t *testing.T) { path := sdk.GetConfig().GetFullFundraiserPath() + 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, 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) diff --git a/client/keys/export_test.go b/client/keys/export_test.go index b01bbf823023..b207a71bb159 100644 --- a/client/keys/export_test.go +++ b/client/keys/export_test.go @@ -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)) diff --git a/client/keys/import_test.go b/client/keys/import_test.go index 1c8d01cb162f..ea84408c2df5 100644 --- a/client/keys/import_test.go +++ b/client/keys/import_test.go @@ -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) diff --git a/client/keys/list.go b/client/keys/list.go index de7681acc9c0..d6c2511b6484 100644 --- a/client/keys/list.go +++ b/client/keys/list.go @@ -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 } diff --git a/client/keys/show_test.go b/client/keys/show_test.go index 798dfc7738fc..7f43450b81c2 100644 --- a/client/keys/show_test.go +++ b/client/keys/show_test.go @@ -112,7 +112,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"}) diff --git a/client/utils.go b/client/utils.go index 99a5be2de42f..b833d4c1f0c5 100644 --- a/client/utils.go +++ b/client/utils.go @@ -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" @@ -67,3 +68,11 @@ func ReadPageRequest(flagSet *pflag.FlagSet) (*query.PageRequest, error) { CountTotal: countTotal, }, 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") +} diff --git a/simapp/simd/cmd/root.go b/simapp/simd/cmd/root.go index 7f1c542d63ce..2804530ad315 100644 --- a/simapp/simd/cmd/root.go +++ b/simapp/simd/cmd/root.go @@ -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" @@ -46,12 +47,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 } @@ -78,6 +87,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} diff --git a/testutil/network/network.go b/testutil/network/network.go index 1da939c3bd3d..3beb1b90c0ca 100644 --- a/testutil/network/network.go +++ b/testutil/network/network.go @@ -333,6 +333,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).