github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/config/config.go (about)

     1  /*
     2   * Copyright (C) 2019 The "MysteriumNetwork/node" Authors.
     3   *
     4   * This program is free software: you can redistribute it and/or modify
     5   * it under the terms of the GNU General Public License as published by
     6   * the Free Software Foundation, either version 3 of the License, or
     7   * (at your option) any later version.
     8   *
     9   * This program is distributed in the hope that it will be useful,
    10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12   * GNU General Public License for more details.
    13   *
    14   * You should have received a copy of the GNU General Public License
    15   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16   */
    17  
    18  package config
    19  
    20  import (
    21  	"math/big"
    22  	"os"
    23  	"strings"
    24  	"sync"
    25  	"time"
    26  
    27  	"github.com/BurntSushi/toml"
    28  	"github.com/mysteriumnetwork/node/eventbus"
    29  	"github.com/mysteriumnetwork/node/metadata"
    30  	"github.com/mysteriumnetwork/node/utils/jsonutil"
    31  	"github.com/pkg/errors"
    32  	"github.com/rs/zerolog/log"
    33  	"github.com/spf13/cast"
    34  	"github.com/urfave/cli/v2"
    35  )
    36  
    37  // Config stores application configuration in 3 separate maps (listed from the lowest priority to the highest):
    38  //
    39  // • Default values
    40  //
    41  // • User configuration (config.toml)
    42  //
    43  // • CLI flags
    44  type Config struct {
    45  	userConfigLocation string
    46  	defaults           map[string]interface{}
    47  	user               map[string]interface{}
    48  	cli                map[string]interface{}
    49  	eventBus           eventbus.EventBus
    50  	mu                 sync.RWMutex
    51  }
    52  
    53  // Current global configuration instance.
    54  var Current = NewConfig()
    55  
    56  // NewConfig creates a new configuration instance.
    57  func NewConfig() *Config {
    58  	return &Config{
    59  		userConfigLocation: "",
    60  		defaults:           make(map[string]interface{}),
    61  		user:               make(map[string]interface{}),
    62  		cli:                make(map[string]interface{}),
    63  	}
    64  }
    65  
    66  func (cfg *Config) userConfigLoaded() bool {
    67  	cfg.mu.RLock()
    68  	defer cfg.mu.RUnlock()
    69  	return cfg.userConfigLocation != ""
    70  }
    71  
    72  // EnableEventPublishing enables config event publishing to the event bus.
    73  func (cfg *Config) EnableEventPublishing(eb eventbus.EventBus) {
    74  	cfg.mu.Lock()
    75  	defer cfg.mu.Unlock()
    76  	cfg.eventBus = eb
    77  }
    78  
    79  // LoadUserConfig loads and remembers user config location.
    80  func (cfg *Config) LoadUserConfig(location string) error {
    81  	log.Debug().Msg("Loading user configuration: " + location)
    82  	cfg.mu.Lock()
    83  	defer cfg.mu.Unlock()
    84  	cfg.userConfigLocation = location
    85  	_, err := toml.DecodeFile(cfg.userConfigLocation, &cfg.user)
    86  	if err != nil {
    87  		return errors.Wrap(err, "failed to decode configuration file")
    88  	}
    89  	cfgJson, err := jsonutil.ToJson(cfg.user)
    90  	if err != nil {
    91  		return err
    92  	}
    93  	log.Info().Msg("User configuration loaded: \n" + cfgJson)
    94  	return nil
    95  }
    96  
    97  // SaveUserConfig saves user configuration to the file from which it was loaded.
    98  func (cfg *Config) SaveUserConfig() error {
    99  	log.Info().Msg("Saving user configuration")
   100  	cfg.mu.RLock()
   101  	defer cfg.mu.RUnlock()
   102  	if !cfg.userConfigLoaded() {
   103  		return errors.New("user configuration cannot be saved, because it must be loaded first")
   104  	}
   105  	var out strings.Builder
   106  	err := toml.NewEncoder(&out).Encode(cfg.user)
   107  	if err != nil {
   108  		return errors.Wrap(err, "failed to write configuration as toml")
   109  	}
   110  	err = os.WriteFile(cfg.userConfigLocation, []byte(out.String()), 0700)
   111  	if err != nil {
   112  		return errors.Wrap(err, "failed to write configuration to file")
   113  	}
   114  	cfgJson, err := jsonutil.ToJson(cfg.user)
   115  	if err != nil {
   116  		return err
   117  	}
   118  	log.Info().Msg("User configuration written: \n" + cfgJson)
   119  	return nil
   120  }
   121  
   122  // GetDefaultConfig returns default configuration.
   123  func (cfg *Config) GetDefaultConfig() map[string]interface{} {
   124  	cfg.mu.RLock()
   125  	defer cfg.mu.RUnlock()
   126  	return deepCopyStrMap(cfg.defaults)
   127  }
   128  
   129  // GetUserConfig returns user configuration.
   130  func (cfg *Config) GetUserConfig() map[string]interface{} {
   131  	cfg.mu.RLock()
   132  	defer cfg.mu.RUnlock()
   133  	return deepCopyStrMap(cfg.user)
   134  }
   135  
   136  // GetConfig returns current configuration.
   137  func (cfg *Config) GetConfig() map[string]interface{} {
   138  	cfg.mu.RLock()
   139  	defer cfg.mu.RUnlock()
   140  	config := make(map[string]interface{})
   141  	mergeMaps(deepCopyStrMap(cfg.defaults), config, nil)
   142  	mergeMaps(deepCopyStrMap(cfg.user), config, nil)
   143  	mergeMaps(deepCopyStrMap(cfg.cli), config, nil)
   144  	return deepCopyStrMap(config)
   145  }
   146  
   147  // SetDefault sets default value for key.
   148  func (cfg *Config) SetDefault(key string, value interface{}) {
   149  	cfg.set(cfg.defaults, key, value)
   150  }
   151  
   152  // SetUser sets user configuration value for key.
   153  func (cfg *Config) SetUser(key string, value interface{}) {
   154  	func() {
   155  		cfg.mu.RLock()
   156  		defer cfg.mu.RUnlock()
   157  		if cfg.eventBus != nil {
   158  			cfg.eventBus.Publish(AppTopicConfig(key), value)
   159  		}
   160  	}()
   161  	cfg.set(cfg.user, key, value)
   162  }
   163  
   164  // SetCLI sets value passed via CLI flag for key.
   165  func (cfg *Config) SetCLI(key string, value interface{}) {
   166  	cfg.set(cfg.cli, key, value)
   167  }
   168  
   169  // RemoveUser removes user configuration value for key.
   170  func (cfg *Config) RemoveUser(key string) {
   171  	cfg.remove(cfg.user, key)
   172  }
   173  
   174  // RemoveCLI removes configured CLI flag value by key.
   175  func (cfg *Config) RemoveCLI(key string) {
   176  	cfg.remove(cfg.cli, key)
   177  }
   178  
   179  // set sets value to a particular configuration value map.
   180  func (cfg *Config) set(configMap map[string]interface{}, key string, value interface{}) {
   181  	key = strings.ToLower(key)
   182  	segments := strings.Split(key, ".")
   183  
   184  	lastKey := strings.ToLower(segments[len(segments)-1])
   185  
   186  	cfg.mu.Lock()
   187  	defer cfg.mu.Unlock()
   188  	deepestMap := deepSearch(configMap, segments[0:len(segments)-1])
   189  
   190  	// set innermost value
   191  	deepestMap[lastKey] = value
   192  }
   193  
   194  // remove removes a configured value from a particular configuration map.
   195  func (cfg *Config) remove(configMap map[string]interface{}, key string) {
   196  	key = strings.ToLower(key)
   197  	segments := strings.Split(key, ".")
   198  
   199  	lastKey := strings.ToLower(segments[len(segments)-1])
   200  
   201  	cfg.mu.Lock()
   202  	defer cfg.mu.Unlock()
   203  	deepestMap := deepSearch(configMap, segments[0:len(segments)-1])
   204  
   205  	// set innermost value
   206  	delete(deepestMap, lastKey)
   207  }
   208  
   209  // Get returns stored config value as-is.
   210  func (cfg *Config) Get(key string) interface{} {
   211  	segments := strings.Split(strings.ToLower(key), ".")
   212  	cfg.mu.RLock()
   213  	defer cfg.mu.RUnlock()
   214  	cliValue := SearchMap(cfg.cli, segments)
   215  	if cliValue != nil {
   216  		log.Debug().Msgf("Returning CLI value %v:%v", key, cliValue)
   217  		return copyValue(cliValue)
   218  	}
   219  	userValue := SearchMap(cfg.user, segments)
   220  	if userValue != nil {
   221  		log.Debug().Msgf("Returning user config value %v:%v", key, userValue)
   222  		return copyValue(userValue)
   223  	}
   224  	defaultValue := SearchMap(cfg.defaults, segments)
   225  	log.Trace().Msgf("Returning default value %v:%v", key, defaultValue)
   226  	return copyValue(defaultValue)
   227  }
   228  
   229  // returns scalar values as is. deep-copies maps.
   230  func copyValue(value interface{}) interface{} {
   231  	switch v := value.(type) {
   232  	case map[string]interface{}:
   233  		return deepCopyStrMap(v)
   234  	default:
   235  		return v
   236  	}
   237  }
   238  
   239  // deepcopy for nested config structures.
   240  func deepCopyStrMap(src map[string]interface{}) map[string]interface{} {
   241  	dst := make(map[string]interface{})
   242  	for sk, sv := range src {
   243  		switch typedValue := sv.(type) {
   244  		case map[string]interface{}:
   245  			dst[sk] = deepCopyStrMap(typedValue)
   246  		default:
   247  			dst[sk] = typedValue
   248  		}
   249  	}
   250  	return dst
   251  }
   252  
   253  // GetBool returns config value as bool.
   254  func (cfg *Config) GetBool(key string) bool {
   255  	return cast.ToBool(cfg.Get(key))
   256  }
   257  
   258  // GetInt returns config value as int.
   259  func (cfg *Config) GetInt(key string) int {
   260  	return cast.ToInt(cfg.Get(key))
   261  }
   262  
   263  // GetInt64 returns config value as int64.
   264  func (cfg *Config) GetInt64(key string) int64 {
   265  	return cast.ToInt64(cfg.Get(key))
   266  }
   267  
   268  // GetUInt64 returns config value as uint64.
   269  func (cfg *Config) GetUInt64(key string) uint64 {
   270  	return cast.ToUint64(cfg.Get(key))
   271  }
   272  
   273  // GetFloat64 returns config value as float64.
   274  func (cfg *Config) GetFloat64(key string) float64 {
   275  	return cast.ToFloat64(cfg.Get(key))
   276  }
   277  
   278  // GetDuration returns config value as duration.
   279  func (cfg *Config) GetDuration(key string) time.Duration {
   280  	return cast.ToDuration(cfg.Get(key))
   281  }
   282  
   283  // GetString returns config value as string.
   284  func (cfg *Config) GetString(key string) string {
   285  	return cast.ToString(cfg.Get(key))
   286  }
   287  
   288  // GetStringSlice returns config value as []string.
   289  func (cfg *Config) GetStringSlice(key string) []string {
   290  	return cast.ToStringSlice(cfg.Get(key))
   291  }
   292  
   293  // ParseBoolFlag parses a cli.BoolFlag from command's context and
   294  // sets default and CLI values to the application configuration.
   295  func (cfg *Config) ParseBoolFlag(ctx *cli.Context, flag cli.BoolFlag) {
   296  	cfg.SetDefault(flag.Name, flag.Value)
   297  	if ctx.IsSet(flag.Name) {
   298  		cfg.SetCLI(flag.Name, ctx.Bool(flag.Name))
   299  	} else {
   300  		cfg.RemoveCLI(flag.Name)
   301  	}
   302  }
   303  
   304  // ParseIntFlag parses a cli.IntFlag from command's context and
   305  // sets default and CLI values to the application configuration.
   306  func (cfg *Config) ParseIntFlag(ctx *cli.Context, flag cli.IntFlag) {
   307  	cfg.SetDefault(flag.Name, flag.Value)
   308  	if ctx.IsSet(flag.Name) {
   309  		cfg.SetCLI(flag.Name, ctx.Int(flag.Name))
   310  	} else {
   311  		cfg.RemoveCLI(flag.Name)
   312  	}
   313  }
   314  
   315  // ParseUInt64Flag parses a cli.Uint64Flag from command's context and
   316  // sets default and CLI values to the application configuration.
   317  func (cfg *Config) ParseUInt64Flag(ctx *cli.Context, flag cli.Uint64Flag) {
   318  	cfg.SetDefault(flag.Name, flag.Value)
   319  	if ctx.IsSet(flag.Name) {
   320  		cfg.SetCLI(flag.Name, ctx.Uint64(flag.Name))
   321  	} else {
   322  		cfg.RemoveCLI(flag.Name)
   323  	}
   324  }
   325  
   326  // ParseInt64Flag parses a cli.Int64Flag from command's context and
   327  // sets default and CLI values to the application configuration.
   328  func (cfg *Config) ParseInt64Flag(ctx *cli.Context, flag cli.Int64Flag) {
   329  	cfg.SetDefault(flag.Name, flag.Value)
   330  	if ctx.IsSet(flag.Name) {
   331  		cfg.SetCLI(flag.Name, ctx.Int64(flag.Name))
   332  	} else {
   333  		cfg.RemoveCLI(flag.Name)
   334  	}
   335  }
   336  
   337  // ParseFloat64Flag parses a cli.Float64Flag from command's context and
   338  // sets default and CLI values to the application configuration.
   339  func (cfg *Config) ParseFloat64Flag(ctx *cli.Context, flag cli.Float64Flag) {
   340  	cfg.SetDefault(flag.Name, flag.Value)
   341  	if ctx.IsSet(flag.Name) {
   342  		cfg.SetCLI(flag.Name, ctx.Float64(flag.Name))
   343  	} else {
   344  		cfg.RemoveCLI(flag.Name)
   345  	}
   346  }
   347  
   348  // ParseDurationFlag parses a cli.DurationFlag from command's context and
   349  // sets default and CLI values to the application configuration.
   350  func (cfg *Config) ParseDurationFlag(ctx *cli.Context, flag cli.DurationFlag) {
   351  	cfg.SetDefault(flag.Name, flag.Value)
   352  	if ctx.IsSet(flag.Name) {
   353  		cfg.SetCLI(flag.Name, ctx.Duration(flag.Name))
   354  	} else {
   355  		cfg.RemoveCLI(flag.Name)
   356  	}
   357  }
   358  
   359  // ParseStringFlag parses a cli.StringFlag from command's context and
   360  // sets default and CLI values to the application configuration.
   361  func (cfg *Config) ParseStringFlag(ctx *cli.Context, flag cli.StringFlag) {
   362  	cfg.SetDefault(flag.Name, flag.Value)
   363  	if ctx.IsSet(flag.Name) {
   364  		cfg.SetCLI(flag.Name, ctx.String(flag.Name))
   365  	} else {
   366  		cfg.RemoveCLI(flag.Name)
   367  	}
   368  }
   369  
   370  // ParseStringSliceFlag parses a cli.StringSliceFlag from command's context and
   371  // sets default and CLI values to the application configuration.
   372  func (cfg *Config) ParseStringSliceFlag(ctx *cli.Context, flag cli.StringSliceFlag) {
   373  	var value []string = nil
   374  	if flag.Value != nil {
   375  		value = flag.Value.Value()
   376  	}
   377  	cfg.SetDefault(flag.Name, value)
   378  	if ctx.IsSet(flag.Name) {
   379  		cfg.SetCLI(flag.Name, ctx.StringSlice(flag.Name))
   380  	} else {
   381  		cfg.RemoveCLI(flag.Name)
   382  	}
   383  }
   384  
   385  // ParseBlockchainNetworkFlag parses a cli.StringFlag as a blockchain network
   386  // from command's context and sets default values for network parameters
   387  // and CLI values for the network to the application configuration.
   388  func (cfg *Config) ParseBlockchainNetworkFlag(ctx *cli.Context, flag cli.StringFlag) {
   389  	cfg.SetDefault(flag.Name, flag.Value)
   390  	if ctx.IsSet(flag.Name) {
   391  		network, err := ParseBlockchainNetwork(ctx.String(flag.Name))
   392  		if err != nil {
   393  			log.Err(err).Msg("invalid network option used as flag, ignoring")
   394  			cfg.RemoveCLI(flag.Name)
   395  			return
   396  		}
   397  		cfg.SetCLI(flag.Name, strings.ToLower(string(network)))
   398  		cfg.SetDefaultsByNetwork(network)
   399  	} else {
   400  		cfg.RemoveCLI(flag.Name)
   401  	}
   402  }
   403  
   404  // SetDefaultsByNetwork sets defaults in config according to the given blockchain network.
   405  func (cfg *Config) SetDefaultsByNetwork(network BlockchainNetwork) {
   406  	var flags map[string]any
   407  	switch network {
   408  	case Mainnet:
   409  		flags = metadata.MainnetDefinition.GetDefaultFlagValues()
   410  	case Testnet:
   411  		flags = metadata.TestnetDefinition.GetDefaultFlagValues()
   412  	case Localnet:
   413  		flags = metadata.LocalnetDefinition.GetDefaultFlagValues()
   414  	default:
   415  		log.Error().Str("network", string(network)).Msg("cannot handle this blockchain network option, ignoring")
   416  		return
   417  	}
   418  	for flagName, flagValue := range flags {
   419  		cfg.SetDefault(flagName, flagValue)
   420  	}
   421  }
   422  
   423  // GetBool shorthand for getting current configuration value for cli.BoolFlag.
   424  func GetBool(flag cli.BoolFlag) bool {
   425  	return Current.GetBool(flag.Name)
   426  }
   427  
   428  // GetInt shorthand for getting current configuration value for cli.IntFlag.
   429  func GetInt(flag cli.IntFlag) int {
   430  	return Current.GetInt(flag.Name)
   431  }
   432  
   433  // GetInt64 shorthand for getting current configuration value for cli.IntFlag.
   434  func GetInt64(flag cli.Int64Flag) int64 {
   435  	return Current.GetInt64(flag.Name)
   436  }
   437  
   438  // GetString shorthand for getting current configuration value for cli.StringFlag.
   439  func GetString(flag cli.StringFlag) string {
   440  	return Current.GetString(flag.Name)
   441  }
   442  
   443  // GetBlockchainNetwork shorthand for getting current configuration value for blockchain network.
   444  func GetBlockchainNetwork(flag cli.StringFlag) BlockchainNetwork {
   445  	return BlockchainNetwork(Current.GetString(flag.Name))
   446  }
   447  
   448  // GetStringSlice shorthand for getting current configuration value for cli.StringSliceFlag.
   449  func GetStringSlice(flag cli.StringSliceFlag) []string {
   450  	return Current.GetStringSlice(flag.Name)
   451  }
   452  
   453  // GetDuration shorthand for getting current configuration value for cli.DurationFlag.
   454  func GetDuration(flag cli.DurationFlag) time.Duration {
   455  	return Current.GetDuration(flag.Name)
   456  }
   457  
   458  // GetUInt64 shorthand for getting current configuration value for cli.Uint64Flag.
   459  func GetUInt64(flag cli.Uint64Flag) uint64 {
   460  	return Current.GetUInt64(flag.Name)
   461  }
   462  
   463  // GetBigInt shorthand for getting and parsing a configuration value for cli.StringFlag that's a big.Int.
   464  func GetBigInt(flag cli.StringFlag) *big.Int {
   465  	b, _ := new(big.Int).SetString(Current.GetString(flag.Name), 10)
   466  	return b
   467  }
   468  
   469  // GetFloat64 shorthand for getting current configuration value for cli.Uint64Flag.
   470  func GetFloat64(flag cli.Float64Flag) float64 {
   471  	return Current.GetFloat64(flag.Name)
   472  }
   473  
   474  // AppTopicConfig returns event bus topic for the given config key to listen for its updates.
   475  func AppTopicConfig(configKey string) string {
   476  	return "config:" + configKey
   477  }