Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add client config #8820

Closed
Closed
183 changes: 183 additions & 0 deletions client/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package config

import (
"encoding/json"
"errors"
"fmt"
"os"
"path"
"strconv"

"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/cosmos/cosmos-sdk/client/flags"
tmcli "github.com/tendermint/tendermint/libs/cli"
)

// Default constants
const (
chainID = ""
keyringBackend = "os"
output = "text"
node = "tcp://localhost:26657"
broadcastMode = "sync"
trace = false
)

var ErrWrongNumberOfArgs = fmt.Errorf("wrong number of arguments")

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"`
Trace bool `mapstructure:"trace" json:"trace"`
}

// TODO Validate values in setters
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
}

func (c *ClientConfig) SetTrace(trace string) error {
amaury1093 marked this conversation as resolved.
Show resolved Hide resolved
boolVal, err := strconv.ParseBool(trace)
if err != nil {
return err
}
c.Trace = boolVal
return nil
}

func DefaultClientConfig() *ClientConfig {
return &ClientConfig{chainID, keyringBackend, output, node, broadcastMode, trace}
}

// Cmd returns a CLI command to interactively create an application CLI
// config file.
func Cmd(defaultCLIHome string) *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),
}

cmd.Flags().String(flags.FlagHome, defaultCLIHome,
"set client's home directory for configuration")
return cmd
}

func runConfigCmd(cmd *cobra.Command, args []string) error {

v := viper.New()

cfgPath, err := ensureCfgPath(v.GetString(flags.FlagHome))
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to make config path: %v\n", err)
return err
}

cliConfig, err := getClientConfig(cfgPath, v)
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to get client config: %v\n", err)
return err
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alessio Following your suggestion, I print an error here to os.Stderr. I am unable to add os.Exit(1) cause the runConfigCmd is supposed to return an error.

amaury1093 marked this conversation as resolved.
Show resolved Hide resolved
}

switch len(args) {
case 0:
amaury1093 marked this conversation as resolved.
Show resolved Hide resolved
// print all client config fields to sdt out
s, _ := json.MarshalIndent(cliConfig, "", "\t")
fmt.Print(string(s))
amaury1093 marked this conversation as resolved.
Show resolved Hide resolved

case 1:
// it's a get
// TODO implement method for get
// should i implement getters here?
key := args[0]
switch key {
case flags.FlagChainID:
fmt.Println(cliConfig.ChainID)
case flags.FlagKeyringBackend:
fmt.Println(cliConfig.KeyringBackend)
case tmcli.OutputFlag:
fmt.Println(cliConfig.Output)
case flags.FlagNode:
fmt.Println(cliConfig.Node)
case flags.FlagBroadcastMode:
fmt.Println(cliConfig.BroadcastMode)
case "trace":
amaury1093 marked this conversation as resolved.
Show resolved Hide resolved
fmt.Println(cliConfig.Trace)
default:
err := errUnknownConfigKey(key)
fmt.Fprintf(os.Stderr, "Unable to get the value for the key: %v, error: %v\n", key, err)
return err
amaury1093 marked this conversation as resolved.
Show resolved Hide resolved
}

case 2:
// it's set

key, value := args[0], args[1]

switch key {
case flags.FlagChainID:
cliConfig.SetChainID(value)
case flags.FlagKeyringBackend:
cliConfig.SetKeyringBackend(value)
case tmcli.OutputFlag:
cliConfig.SetOutput(value)
case flags.FlagNode:
cliConfig.SetNode(value)
case flags.FlagBroadcastMode:
cliConfig.SetBroadcastMode(value)
case "trace":
if err := cliConfig.SetTrace(value); err != nil {
fmt.Fprintf(os.Stderr, "Unable to parse value to bool, err: %v\n", err)
return err
amaury1093 marked this conversation as resolved.
Show resolved Hide resolved
}
default:
return errUnknownConfigKey(key)
}

configTemplate, err := InitConfigTemplate()
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to initiate config template, err: %v\n", err)
return err
amaury1093 marked this conversation as resolved.
Show resolved Hide resolved
}

cfgFile := path.Join(cfgPath, "config.toml")
if err := WriteConfigFile(cfgFile, cliConfig, configTemplate); err != nil {
fmt.Fprintf(os.Stderr, "Unable to write client config to the file, err: %v\n", err)
amaury1093 marked this conversation as resolved.
Show resolved Hide resolved
return err
}

default:
// print error
err := errors.New("unable to execute config command")
fmt.Fprintf(os.Stderr, "%v\n", err)
return err
amaury1093 marked this conversation as resolved.
Show resolved Hide resolved
}

return nil
}

func errUnknownConfigKey(key string) error {
return fmt.Errorf("unknown configuration key: %q", key)
}
61 changes: 61 additions & 0 deletions client/config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package config

import (
"io/ioutil"
"os"
"path/filepath"
"testing"

"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/cosmos/cosmos-sdk/client/flags"
)

// For https://github.com/cosmos/cosmos-sdk/issues/3899
func Test_runConfigCmdTwiceWithShorterNodeValue(t *testing.T) {
// Prepare environment
t.Parallel()
configHome, cleanup := tmpDir(t)
defer cleanup()
_ = os.RemoveAll(filepath.Join(configHome, "config"))
viper.Set(flags.FlagHome, configHome)

// Init command config
cmd := Cmd(configHome)
assert.NotNil(t, cmd)

err := cmd.RunE(cmd, []string{"node", "tcp://localhost:26657"})
assert.Nil(t, err)

err = cmd.RunE(cmd, []string{"node", "--get"})
assert.Nil(t, err)

err = cmd.RunE(cmd, []string{"node", "tcp://local:26657"})
assert.Nil(t, err)

err = cmd.RunE(cmd, []string{"node", "--get"})
assert.Nil(t, err)

err = cmd.RunE(cmd, nil)
assert.Nil(t, err)

err = cmd.RunE(cmd, []string{"invalidKey", "--get"})
require.Equal(t, err, errUnknownConfigKey("invalidKey"))

err = cmd.RunE(cmd, []string{"invalidArg1"})
require.Equal(t, err, ErrWrongNumberOfArgs)

err = cmd.RunE(cmd, []string{"invalidKey", "invalidValue"})
require.Equal(t, err, errUnknownConfigKey("invalidKey"))

// TODO add testing of pririty environmental variable, flag and file

}

func tmpDir(t *testing.T) (string, func()) {
dir, err := ioutil.TempDir("", t.Name()+"_")
require.NoError(t, err)
return dir, func() { _ = os.RemoveAll(dir) }
}
90 changes: 90 additions & 0 deletions client/config/toml.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package config

import (
"bytes"
"os"
"path"
"text/template"

"github.com/spf13/viper"
tmos "github.com/tendermint/tendermint/libs/os"
)

const defaultConfigTemplate = `# This is a TOML config file.
# For more information, see https://github.com/toml-lang/toml

###############################################################################
### Client Configuration ###
###############################################################################


chain-id = "{{ .ClientConfig.ChainID }}"
amaury1093 marked this conversation as resolved.
Show resolved Hide resolved
keyring-backend = "{{ .ClientConfig.KeyringBackend }}"
output = "{{ .ClientConfig.Output }}"
node = "{{ .ClientConfig.Node }}"
broadcast-mode = "{{ .ClientConfig.BroadcastMode }}"
trace = "{{ .ClientConfig.Trace }}"
`

// InitConfigTemplate initiates config template that will be used in
// WriteConfigFile
func InitConfigTemplate() (*template.Template, error) {
tmpl := template.New("clientConfigFileTemplate")
configTemplate, err := tmpl.Parse(defaultConfigTemplate)
if err != nil {
return nil, err
}

return configTemplate, nil
}

// ParseConfig retrieves the default environment configuration for the
// application.
func ParseConfig(v *viper.Viper) (*ClientConfig, error) {
conf := DefaultClientConfig()
if err := v.Unmarshal(conf); err != nil {
return nil, err
}

return conf, nil
}

// WriteConfigFile renders config using the template and writes it to
// configFilePath.
func WriteConfigFile(cfgFile string, config *ClientConfig, configTemplate *template.Template) error {
var buffer bytes.Buffer

if err := configTemplate.Execute(&buffer, config); err != nil {
return err
}

tmos.MustWriteFile(cfgFile, buffer.Bytes(), 0644)
return nil

}

func ensureCfgPath(rootDir string) (string, error) {
cfgPath := path.Join(rootDir, "config")
if err := os.MkdirAll(cfgPath, os.ModePerm); err != nil { // config directory
return "", err
}

return cfgPath, nil
}

func getClientConfig(cfgPath string, v *viper.Viper) (*ClientConfig, error) {
amaury1093 marked this conversation as resolved.
Show resolved Hide resolved
v.AddConfigPath(cfgPath)
v.SetConfigName("client")
v.SetConfigType("toml")
v.AutomaticEnv()
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
}
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ require (
github.com/improbable-eng/grpc-web v0.14.0
github.com/magiconair/properties v1.8.4
github.com/mattn/go-isatty v0.0.12
github.com/mitchellh/mapstructure v1.3.3
github.com/otiai10/copy v1.5.0
github.com/pelletier/go-toml v1.8.1 // indirect
github.com/pelletier/go-toml v1.8.1
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.9.0
github.com/prometheus/common v0.18.0
Expand Down
13 changes: 13 additions & 0 deletions server/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"syscall"
"time"

clicfg "github.com/cosmos/cosmos-sdk/client/config"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -242,6 +243,18 @@ func interceptConfigs(rootViper *viper.Viper) (*tmcfg.Config, error) {
return nil, fmt.Errorf("failed to merge configuration: %w", err)
}

// Adding default ClientConfig and writing it into "client.toml"
cliCfgFilePath := filepath.Join(configPath, "client.toml")
amaury1093 marked this conversation as resolved.
Show resolved Hide resolved
if _, err := os.Stat(cliCfgFilePath); os.IsNotExist(err) {
amaury1093 marked this conversation as resolved.
Show resolved Hide resolved
cliConfig, err := clicfg.ParseConfig(rootViper)
if err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", cliCfgFilePath, err)
}

configTemplate := clicfg.InitConfigTemplate()
clicfg.WriteConfigFile(cliCfgFilePath, cliConfig, configTemplate)
}

return conf, nil
}

Expand Down
Loading