github.com/iotexproject/iotex-core@v1.14.1-rc1/ioctl/newcmd/config/config.go (about)

     1  // Copyright (c) 2022 IoTeX Foundation
     2  // This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability
     3  // or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed.
     4  // This source code is governed by Apache License 2.0 that can be found in the LICENSE file.
     5  
     6  package config
     7  
     8  import (
     9  	"encoding/json"
    10  	"fmt"
    11  	"os"
    12  	"path/filepath"
    13  	"regexp"
    14  	"strconv"
    15  	"strings"
    16  
    17  	"github.com/pkg/errors"
    18  	"github.com/spf13/cobra"
    19  	"golang.org/x/text/cases"
    20  	"golang.org/x/text/language"
    21  	"gopkg.in/yaml.v2"
    22  
    23  	serverCfg "github.com/iotexproject/iotex-core/config"
    24  	"github.com/iotexproject/iotex-core/ioctl"
    25  	"github.com/iotexproject/iotex-core/ioctl/config"
    26  	"github.com/iotexproject/iotex-core/ioctl/validator"
    27  )
    28  
    29  // Regexp patterns
    30  const (
    31  	_ipPattern               = `((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)`
    32  	_domainPattern           = `[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}(\.[a-zA-Z0-9][a-zA-Z0-9_-]{0,62})*(\.[a-zA-Z][a-zA-Z0-9]{0,10}){1}`
    33  	_localPattern            = "localhost"
    34  	_endpointPattern         = "(" + _ipPattern + "|(" + _domainPattern + ")" + "|(" + _localPattern + "))" + `(:\d{1,5})?`
    35  	_defaultAnalyserEndpoint = "https://iotex-analyser-api-mainnet.chainanalytics.org"
    36  	_defaultConfigFileName   = "config.default"
    37  	// _defaultWsEndpoint default w3bstream endpoint
    38  	_defaultWsEndpoint = "sprout-staging.w3bstream.com:9000"
    39  	// _defaultIPFSEndpoint default IPFS endpoint for uploading
    40  	_defaultIPFSEndpoint = "ipfs.mainnet.iotex.io"
    41  	// _defaultIPFSGateway default IPFS gateway for resource fetching
    42  	_defaultIPFSGateway = "https://ipfs.io"
    43  	// _defaultWsRegisterContract default w3bstream project register contract address
    44  	_defaultWsRegisterContract = "0x184C72E39a642058CCBc369485c7fd614B40a03d"
    45  )
    46  
    47  var (
    48  	_supportedLanguage = []string{"English", "中文"}
    49  	_validArgs         = []string{"endpoint", "wallet", "explorer", "defaultacc", "language", "nsv2height", "wsEndpoint", "ipfsEndpoint", "ipfsGateway", "wsRegisterContract"}
    50  	_validGetArgs      = []string{"endpoint", "wallet", "explorer", "defaultacc", "language", "nsv2height", "wsEndpoint", "ipfsEndpoint", "ipfsGateway", "analyserEndpoint", "wsRegisterContract", "all"}
    51  	_validExpl         = []string{"iotexscan", "iotxplorer"}
    52  	_endpointCompile   = regexp.MustCompile("^" + _endpointPattern + "$")
    53  	_configDir         = os.Getenv("HOME") + "/.config/ioctl/default"
    54  )
    55  
    56  // Multi-language support
    57  var (
    58  	_configCmdShorts = map[config.Language]string{
    59  		config.English: "Manage the configuration of ioctl",
    60  		config.Chinese: "ioctl配置管理",
    61  	}
    62  )
    63  
    64  // NewConfigCmd represents the new node command.
    65  func NewConfigCmd(client ioctl.Client) *cobra.Command {
    66  	configShorts, _ := client.SelectTranslation(_configCmdShorts)
    67  
    68  	cmd := &cobra.Command{
    69  		Use:   "config",
    70  		Short: configShorts,
    71  	}
    72  	cmd.AddCommand(NewConfigSetCmd(client))
    73  	cmd.AddCommand(NewConfigGetCmd(client))
    74  	cmd.AddCommand(NewConfigResetCmd(client))
    75  
    76  	return cmd
    77  }
    78  
    79  // info contains the information of config file
    80  type info struct {
    81  	readConfig        config.Config
    82  	defaultConfigFile string // Path to config file
    83  }
    84  
    85  // InitConfig load config data from default config file
    86  func InitConfig() (config.Config, string, error) {
    87  	info := &info{
    88  		readConfig: config.Config{
    89  			Aliases: make(map[string]string),
    90  		},
    91  	}
    92  
    93  	// Create path to config directory
    94  	err := os.MkdirAll(_configDir, 0700)
    95  	if err != nil {
    96  		return info.readConfig, info.defaultConfigFile, err
    97  	}
    98  	info.defaultConfigFile = filepath.Join(_configDir, _defaultConfigFileName)
    99  
   100  	// Load or reset config file
   101  	err = info.loadConfig()
   102  	if os.IsNotExist(err) {
   103  		err = info.reset()
   104  	}
   105  	if err != nil {
   106  		return info.readConfig, info.defaultConfigFile, err
   107  	}
   108  
   109  	// Check completeness of config file
   110  	completeness := true
   111  	if info.readConfig.Wallet == "" {
   112  		info.readConfig.Wallet = _configDir
   113  		completeness = false
   114  	}
   115  	if info.readConfig.Language == "" {
   116  		info.readConfig.Language = _supportedLanguage[0]
   117  		completeness = false
   118  	}
   119  	if info.readConfig.Nsv2height == 0 {
   120  		info.readConfig.Nsv2height = serverCfg.Default.Genesis.FairbankBlockHeight
   121  	}
   122  	if info.readConfig.AnalyserEndpoint == "" {
   123  		info.readConfig.AnalyserEndpoint = _defaultAnalyserEndpoint
   124  		completeness = false
   125  	}
   126  	if info.readConfig.WsEndpoint == "" {
   127  		info.readConfig.WsEndpoint = _defaultWsEndpoint
   128  		completeness = false
   129  	}
   130  	if info.readConfig.IPFSEndpoint == "" {
   131  		info.readConfig.IPFSEndpoint = _defaultIPFSEndpoint
   132  	}
   133  	if info.readConfig.IPFSGateway == "" {
   134  		info.readConfig.IPFSGateway = _defaultIPFSGateway
   135  	}
   136  	if info.readConfig.WsRegisterContract == "" {
   137  		info.readConfig.WsRegisterContract = _defaultWsRegisterContract
   138  	}
   139  	if !completeness {
   140  		if err = info.writeConfig(); err != nil {
   141  			return info.readConfig, info.defaultConfigFile, err
   142  		}
   143  	}
   144  	// Set language for ioctl
   145  	if isSupportedLanguage(info.readConfig.Language) == -1 {
   146  		fmt.Printf("Warn: Language %s is not supported, English instead.\n", info.readConfig.Language)
   147  	}
   148  	return info.readConfig, info.defaultConfigFile, nil
   149  }
   150  
   151  // newInfo create config info
   152  func newInfo(readConfig config.Config, defaultConfigFile string) *info {
   153  	return &info{
   154  		readConfig:        readConfig,
   155  		defaultConfigFile: defaultConfigFile,
   156  	}
   157  }
   158  
   159  // reset resets all values of config
   160  func (c *info) reset() error {
   161  	c.readConfig.Wallet = filepath.Dir(c.defaultConfigFile)
   162  	c.readConfig.Endpoint = ""
   163  	c.readConfig.SecureConnect = true
   164  	c.readConfig.DefaultAccount = *new(config.Context)
   165  	c.readConfig.Explorer = _validExpl[0]
   166  	c.readConfig.Language = _supportedLanguage[0]
   167  	c.readConfig.AnalyserEndpoint = _defaultAnalyserEndpoint
   168  	c.readConfig.WsEndpoint = _defaultWsEndpoint
   169  	c.readConfig.IPFSEndpoint = _defaultIPFSEndpoint
   170  	c.readConfig.IPFSGateway = _defaultIPFSGateway
   171  	c.readConfig.WsRegisterContract = _defaultWsRegisterContract
   172  
   173  	err := c.writeConfig()
   174  	if err != nil {
   175  		return err
   176  	}
   177  
   178  	fmt.Println("Config set to default values")
   179  	return nil
   180  }
   181  
   182  // set sets config variable
   183  func (c *info) set(args []string, insecure bool, client ioctl.Client) (string, error) {
   184  	switch args[0] {
   185  	case "endpoint":
   186  		if !isValidEndpoint(args[1]) {
   187  			return "", errors.Errorf("endpoint %s is not valid", args[1])
   188  		}
   189  		c.readConfig.Endpoint = args[1]
   190  		c.readConfig.SecureConnect = !insecure
   191  	case "analyserEndpoint":
   192  		c.readConfig.AnalyserEndpoint = args[1]
   193  	case "wallet":
   194  		c.readConfig.Wallet = args[1]
   195  	case "explorer":
   196  		lowArg := strings.ToLower(args[1])
   197  		switch {
   198  		case isValidExplorer(lowArg):
   199  			c.readConfig.Explorer = lowArg
   200  		case args[1] == "custom":
   201  			link, err := client.ReadCustomLink()
   202  			if err != nil {
   203  				return "", errors.Wrapf(err, "invalid link %s", link)
   204  			}
   205  			c.readConfig.Explorer = link
   206  		default:
   207  			return "", errors.Errorf("explorer %s is not valid\nValid explorers: %s",
   208  				args[1], append(_validExpl, "custom"))
   209  		}
   210  	case "defaultacc":
   211  		if err := validator.ValidateAlias(args[1]); err == nil {
   212  		} else if err = validator.ValidateAddress(args[1]); err == nil {
   213  		} else {
   214  			return "", errors.Errorf("failed to validate alias or address %s", args[1])
   215  		}
   216  		c.readConfig.DefaultAccount.AddressOrAlias = args[1]
   217  	case "language":
   218  		lang := isSupportedLanguage(args[1])
   219  		if lang == -1 {
   220  			return "", errors.Errorf("language %s is not supported\nSupported languages: %s",
   221  				args[1], _supportedLanguage)
   222  		}
   223  		c.readConfig.Language = _supportedLanguage[lang]
   224  	case "nsv2height":
   225  		height, err := strconv.ParseUint(args[1], 10, 64)
   226  		if err != nil {
   227  			return "", errors.Wrapf(err, "invalid height %d", height)
   228  		}
   229  		c.readConfig.Nsv2height = height
   230  	case "wsEndpoint":
   231  		c.readConfig.WsEndpoint = args[1]
   232  	case "ipfsEndpoint":
   233  		c.readConfig.IPFSEndpoint = args[1]
   234  	case "ipfsGateway":
   235  		c.readConfig.IPFSGateway = args[1]
   236  	case "wsRegisterContract":
   237  		c.readConfig.WsRegisterContract = args[1]
   238  	default:
   239  		return "", config.ErrConfigNotMatch
   240  	}
   241  
   242  	err := c.writeConfig()
   243  	if err != nil {
   244  		return "", err
   245  	}
   246  
   247  	return cases.Title(language.Und).String(args[0]) + " is set to " + args[1], nil
   248  }
   249  
   250  // get retrieves a config item from its key.
   251  func (c *info) get(arg string) (string, error) {
   252  	switch arg {
   253  	case "endpoint":
   254  		if c.readConfig.Endpoint == "" {
   255  			return "", config.ErrEmptyEndpoint
   256  		}
   257  		return fmt.Sprintf("%s secure connect(TLS): %t", c.readConfig.Endpoint, c.readConfig.SecureConnect), nil
   258  	case "wallet":
   259  		return c.readConfig.Wallet, nil
   260  	case "defaultacc":
   261  		if c.readConfig.DefaultAccount.AddressOrAlias == "" {
   262  			return "", config.ErrConfigDefaultAccountNotSet
   263  		}
   264  		return jsonString(c.readConfig.DefaultAccount)
   265  	case "explorer":
   266  		return c.readConfig.Explorer, nil
   267  	case "language":
   268  		return c.readConfig.Language, nil
   269  	case "nsv2height":
   270  		return strconv.FormatUint(c.readConfig.Nsv2height, 10), nil
   271  	case "analyserEndpoint":
   272  		return c.readConfig.AnalyserEndpoint, nil
   273  	case "wsEndpoint":
   274  		return c.readConfig.WsEndpoint, nil
   275  	case "ipfsEndpoint":
   276  		return c.readConfig.IPFSEndpoint, nil
   277  	case "ipfsGateway":
   278  		return c.readConfig.IPFSGateway, nil
   279  	case "wsRegisterContract":
   280  		return c.readConfig.WsRegisterContract, nil
   281  	case "all":
   282  		return jsonString(c.readConfig)
   283  	default:
   284  		return "", config.ErrConfigNotMatch
   285  	}
   286  }
   287  
   288  // isValidEndpoint makes sure the endpoint matches the endpoint match pattern
   289  func isValidEndpoint(endpoint string) bool {
   290  	return _endpointCompile.MatchString(endpoint)
   291  }
   292  
   293  // isValidExplorer checks if the explorer is a valid option
   294  func isValidExplorer(arg string) bool {
   295  	for _, exp := range _validExpl {
   296  		if arg == exp {
   297  			return true
   298  		}
   299  	}
   300  	return false
   301  }
   302  
   303  // writeConfig writes to config file
   304  func (c *info) writeConfig() error {
   305  	out, err := yaml.Marshal(&c.readConfig)
   306  	if err != nil {
   307  		return errors.Wrap(err, "failed to marshal config")
   308  	}
   309  	if err := os.WriteFile(c.defaultConfigFile, out, 0600); err != nil {
   310  		return errors.Wrap(err, fmt.Sprintf("failed to write to config file %s", c.defaultConfigFile))
   311  	}
   312  	return nil
   313  }
   314  
   315  // loadConfig loads config file in yaml format
   316  func (c *info) loadConfig() error {
   317  	in, err := os.ReadFile(c.defaultConfigFile)
   318  	if err != nil {
   319  		return err
   320  	}
   321  	if err = yaml.Unmarshal(in, &c.readConfig); err != nil {
   322  		return errors.Wrap(err, "failed to unmarshal config")
   323  	}
   324  	return nil
   325  }
   326  
   327  // isSupportedLanguage checks if the language is a supported option and returns index when supported
   328  func isSupportedLanguage(arg string) config.Language {
   329  	if index, err := strconv.Atoi(arg); err == nil && index >= 0 && index < len(_supportedLanguage) {
   330  		return config.Language(index)
   331  	}
   332  	for i, lang := range _supportedLanguage {
   333  		if strings.EqualFold(arg, lang) {
   334  			return config.Language(i)
   335  		}
   336  	}
   337  	return config.Language(-1)
   338  }
   339  
   340  // jsonString returns json string for message
   341  func jsonString(input interface{}) (string, error) {
   342  	byteAsJSON, err := json.MarshalIndent(input, "", "  ")
   343  	if err != nil {
   344  		return "", errors.Wrap(err, "failed to JSON marshal config field")
   345  	}
   346  	return fmt.Sprint(string(byteAsJSON)), nil
   347  }