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

feat(client): allow overwritting client.toml #17513

Merged
merged 20 commits into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Ref: https://keepachangelog.com/en/1.0.0/

### Features

* (client) [#17513](https://github.com/cosmos/cosmos-sdk/pull/17513) Allow overwritting `client.toml`. Use `client.CreateClientConfigAndOrContext` in place of `client.ReadFromClientConfig` and provide a custom template and a custom config.
* (x/bank) [#14224](https://github.com/cosmos/cosmos-sdk/pull/14224) Allow injection of restrictions on transfers using `AppendSendRestriction` or `PrependSendRestriction`.

### Improvements
Expand Down
87 changes: 54 additions & 33 deletions client/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import (
"path/filepath"

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

func DefaultConfig() *ClientConfig {
return &ClientConfig{
// DefaultConfig returns default config for the client.toml
func DefaultConfig() *Config {
return &Config{
ChainID: "",
KeyringBackend: "os",
Output: "text",
Expand All @@ -18,60 +20,79 @@ func DefaultConfig() *ClientConfig {
}
}

type ClientConfig struct {
// ClientConfig is an alias for Config for backward compatibility
// Deprecated: use Config instead which avoid name stuttering
type ClientConfig Config

type Config 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"`
}

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
// It uses CreateClientConfigAndOrContext internally with no custom template and custom config.
func ReadFromClientConfig(ctx client.Context) (client.Context, error) {
return CreateClientConfigAndOrContext(ctx, "", nil)
}

// ReadFromClientConfig reads values from client.toml file and updates them in client Context
func ReadFromClientConfig(ctx client.Context) (client.Context, error) {
// CreateClientConfigAndOrContext reads the client.toml file and returns a new populated client.Context
// If the client.toml file does not exist, it creates one with default values.
// It takes a customClientTemplate and customConfig as input that can be used to overwrite the default config and enhance the client.toml file.
// The custom template/config must be both provided or be "" and nil.
func CreateClientConfigAndOrContext(ctx client.Context, customClientTemplate string, customConfig interface{}) (client.Context, error) {
julienrbrt marked this conversation as resolved.
Show resolved Hide resolved
configPath := filepath.Join(ctx.HomeDir, "config")
configFilePath := filepath.Join(configPath, "client.toml")
conf := DefaultConfig()

// when config.toml does not exist create and init with default values
if _, err := os.Stat(configFilePath); os.IsNotExist(err) {
if err := os.MkdirAll(configPath, os.ModePerm); err != nil {
return ctx, fmt.Errorf("couldn't make client config: %w", err)
}

if ctx.ChainID != "" {
conf.ChainID = ctx.ChainID // chain-id will be written to the client.toml while initiating the chain.
if (customClientTemplate != "" && customConfig == nil) || (customClientTemplate == "" && customConfig != nil) {
return ctx, fmt.Errorf("customClientTemplate and customConfig should be both nil or not nil")
}

if err := writeConfigToFile(configFilePath, conf); err != nil {
return ctx, fmt.Errorf("could not write client config to the file: %w", err)
if customClientTemplate != "" {
if err := setConfigTemplate(customClientTemplate); err != nil {
return ctx, fmt.Errorf("couldn't set client config template: %w", err)
}

if ctx.ChainID != "" {
// chain-id will be written to the client.toml while initiating the chain.
ctx.Viper.Set(flags.FlagChainID, ctx.ChainID)
}

if err = ctx.Viper.Unmarshal(&customConfig); err != nil {
return ctx, fmt.Errorf("failed to parse custom client config: %w", err)
}

if err := writeConfigFile(configFilePath, customConfig); err != nil {
return ctx, fmt.Errorf("could not write client config to the file: %w", err)
}

} else {
conf := DefaultConfig()
if ctx.ChainID != "" {
// chain-id will be written to the client.toml while initiating the chain.
conf.ChainID = ctx.ChainID
}

if err := writeConfigFile(configFilePath, conf); err != nil {
return ctx, fmt.Errorf("could not write client config to the file: %w", err)
}
}
}

conf, err := getClientConfig(configPath, ctx.Viper)
if err != nil {
return ctx, fmt.Errorf("couldn't get client config: %w", err)
}
// we need to update KeyringDir field on Client Context first cause it is used in NewKeyringFromBackend

// we need to update KeyringDir field on client.Context first cause it is used in NewKeyringFromBackend
ctx = ctx.WithOutputFormat(conf.Output).
WithChainID(conf.ChainID).
WithKeyringDir(ctx.HomeDir)
Expand All @@ -81,17 +102,17 @@ func ReadFromClientConfig(ctx client.Context) (client.Context, error) {
return ctx, fmt.Errorf("couldn't get keyring: %w", 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: %w", err)
}

ctx = ctx.WithNodeURI(conf.Node).
ctx = ctx.
WithNodeURI(conf.Node).
WithBroadcastMode(conf.BroadcastMode).
WithClient(client).
WithBroadcastMode(conf.BroadcastMode)
WithKeyring(keyring)

return ctx, nil
}
102 changes: 96 additions & 6 deletions client/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,27 @@ import (
)

const (
chainID = "test-chain"
nodeEnv = "NODE"
testNode1 = "http://localhost:1"
testNode2 = "http://localhost:2"
)

// initClientContext initiates client Context for tests
// initClientContext initiates client.Context for tests
func initClientContext(t *testing.T, envVar string) (client.Context, func()) {
t.Helper()

clientCtx, cleanup, err := initClientContextWithTemplate(t, envVar, "", nil)
require.NoError(t, err)
require.Equal(t, chainID, clientCtx.ChainID)

return clientCtx, cleanup
}

// initClientContextWithTemplate initiates client.Context with custom config and template for tests
func initClientContextWithTemplate(t *testing.T, envVar, customTemplate string, customConfig interface{}) (client.Context, func(), error) {
t.Helper()
home := t.TempDir()
chainID := "test-chain"
clientCtx := client.Context{}.
WithHomeDir(home).
WithViper("").
Expand All @@ -38,11 +49,89 @@ func initClientContext(t *testing.T, envVar string) (client.Context, func()) {
require.NoError(t, os.Setenv(nodeEnv, envVar))
}

clientCtx, err := config.ReadFromClientConfig(clientCtx)
require.NoError(t, err)
require.Equal(t, clientCtx.ChainID, chainID)
clientCtx, err := config.CreateClientConfigAndOrContext(clientCtx, customTemplate, customConfig)
return clientCtx, func() { _ = os.RemoveAll(home) }, err
}

func TestCustomTemplateAndConfig(t *testing.T) {
type GasConfig struct {
GasAdjustment float64 `mapstructure:"gas-adjustment"`
}

type CustomClientConfig struct {
config.Config `mapstructure:",squash"`

GasConfig GasConfig `mapstructure:"gas"`

Note string `mapstructure:"note"`
}

clientCfg := config.DefaultConfig()
// Overwrite the default keyring backend.
clientCfg.KeyringBackend = "test"

customClientConfig := CustomClientConfig{
Config: *clientCfg,
GasConfig: GasConfig{
GasAdjustment: 1.5,
},
Note: "Sent from the CLI.",
}

customClientConfigTemplate := config.DefaultClientConfigTemplate + `
# This is the gas adjustment factor used by the tx commands.
# Sets the default and can be overwriten by the --gas-adjustment flag in tx commands.
gas-adjustment = {{ .GasConfig.GasAdjustment }}
# Memo to include in all transactions.
note = "{{ .Note }}"
`

t.Run("custom template and config provided", func(t *testing.T) {
clientCtx, cleanup, err := initClientContextWithTemplate(t, "", customClientConfigTemplate, customClientConfig)
defer func() {
cleanup()
_ = os.Unsetenv(nodeEnv)
}()

require.NoError(t, err)
require.Equal(t, customClientConfig.KeyringBackend, clientCtx.Viper.Get(flags.FlagKeyringBackend))
require.Equal(t, customClientConfig.GasConfig.GasAdjustment, clientCtx.Viper.GetFloat64(flags.FlagGasAdjustment))
require.Equal(t, customClientConfig.Note, clientCtx.Viper.GetString(flags.FlagNote))
})

return clientCtx, func() { _ = os.RemoveAll(home) }
t.Run("no template and custom config provided", func(t *testing.T) {
_, cleanup, err := initClientContextWithTemplate(t, "", "", customClientConfig)
defer func() {
cleanup()
_ = os.Unsetenv(nodeEnv)
}()

require.Error(t, err)
})

t.Run("default template and custom config provided", func(t *testing.T) {
clientCtx, cleanup, err := initClientContextWithTemplate(t, "", config.DefaultClientConfigTemplate, customClientConfig)
defer func() {
cleanup()
_ = os.Unsetenv(nodeEnv)
}()

require.NoError(t, err)
require.Equal(t, customClientConfig.KeyringBackend, clientCtx.Viper.Get(flags.FlagKeyringBackend))
require.Nil(t, clientCtx.Viper.Get(flags.FlagGasAdjustment)) // nil because we do not read the flags
})

t.Run("no template and no config provided", func(t *testing.T) {
clientCtx, cleanup, err := initClientContextWithTemplate(t, "", "", nil)
defer func() {
cleanup()
_ = os.Unsetenv(nodeEnv)
}()

require.NoError(t, err)
require.Equal(t, config.DefaultConfig().KeyringBackend, clientCtx.Viper.Get(flags.FlagKeyringBackend))
require.Nil(t, clientCtx.Viper.Get(flags.FlagGasAdjustment)) // nil because we do not read the flags
})
}

func TestConfigCmdEnvFlag(t *testing.T) {
Expand Down Expand Up @@ -79,6 +168,7 @@ func TestConfigCmdEnvFlag(t *testing.T) {
cleanup()
_ = os.Unsetenv(nodeEnv)
}()

/*
env var is set with a flag

Expand Down
34 changes: 25 additions & 9 deletions client/config/toml.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import (
"github.com/spf13/viper"
)

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

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

# The network chain ID
Expand All @@ -27,17 +27,33 @@ node = "{{ .Node }}"
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
var configTemplate *template.Template

func init() {
var err error

tmpl := template.New("clientConfigFileTemplate")
configTemplate, err := tmpl.Parse(defaultConfigTemplate)
if err != nil {
if configTemplate, err = tmpl.Parse(DefaultClientConfigTemplate); err != nil {
panic(err)
}
}

// setConfigTemplate sets the custom app config template for
// the application
func setConfigTemplate(customTemplate string) error {
tmpl := template.New("clientConfigFileTemplate")
var err error
if configTemplate, err = tmpl.Parse(customTemplate); err != nil {
return err
}

return nil
}

// writeConfigFile renders config using the template and writes it to
// configFilePath.
func writeConfigFile(configFilePath string, config interface{}) error {
var buffer bytes.Buffer
if err := configTemplate.Execute(&buffer, config); err != nil {
return err
}
Expand All @@ -46,7 +62,7 @@ func writeConfigToFile(configFilePath string, config *ClientConfig) error {
}

// getClientConfig reads values from client.toml file and unmarshalls them into ClientConfig
func getClientConfig(configPath string, v *viper.Viper) (*ClientConfig, error) {
func getClientConfig(configPath string, v *viper.Viper) (*Config, error) {
v.AddConfigPath(configPath)
v.SetConfigName("client")
v.SetConfigType("toml")
Expand Down
Loading
Loading