github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/config/config.go (about)

     1  package config
     2  
     3  import (
     4  	"bytes"
     5  	_ "embed"
     6  	"errors"
     7  	"fmt"
     8  	"path/filepath"
     9  	"regexp"
    10  	"strings"
    11  
    12  	"github.com/go-playground/validator/v10"
    13  	"github.com/mitchellh/mapstructure"
    14  	"github.com/rs/zerolog"
    15  	"github.com/spf13/pflag"
    16  	"github.com/spf13/viper"
    17  
    18  	"github.com/onflow/flow-go/network/netconf"
    19  )
    20  
    21  var (
    22  	conf     = viper.New()
    23  	validate *validator.Validate
    24  	//go:embed default-config.yml
    25  	configFile string
    26  
    27  	errPflagsNotParsed = errors.New("failed to bind flags to configuration values, pflags must be parsed before binding")
    28  )
    29  
    30  func init() {
    31  	initialize()
    32  }
    33  
    34  // FlowConfig Flow configuration.
    35  type FlowConfig struct {
    36  	// ConfigFile used to set a path to a config.yml file used to override the default-config.yml file.
    37  	ConfigFile    string          `validate:"filepath" mapstructure:"config-file"`
    38  	NetworkConfig *netconf.Config `mapstructure:"network-config"`
    39  }
    40  
    41  // Validate checks validity of the Flow config. Errors indicate that either the configuration is broken,
    42  // incompatible with the node's internal state, or that the node's internal state is corrupted. In all
    43  // cases, continuation is impossible.
    44  func (fc *FlowConfig) Validate() error {
    45  	err := validate.Struct(fc)
    46  	if err != nil {
    47  		if validationErrors, ok := err.(validator.ValidationErrors); ok {
    48  			return fmt.Errorf("failed to validate flow configuration: %w", validationErrors)
    49  		}
    50  		return fmt.Errorf("unexpeceted error encountered while validating flow configuration: %w", err)
    51  	}
    52  	return nil
    53  }
    54  
    55  // DefaultConfig initializes the flow configuration. All default values for the Flow
    56  // configuration are stored in the default-config.yml file. These values can be overridden
    57  // by node operators by setting the corresponding cli flag. DefaultConfig should be called
    58  // before any pflags are parsed, this will allow the configuration to initialize with defaults
    59  // from default-config.yml.
    60  // Returns:
    61  //
    62  //	*FlowConfig: an instance of the network configuration fully initialized to the default values set in the config file
    63  //	error: if there is any error encountered while initializing the configuration, all errors are considered irrecoverable.
    64  func DefaultConfig() (*FlowConfig, error) {
    65  	var flowConfig FlowConfig
    66  	err := Unmarshall(&flowConfig)
    67  	if err != nil {
    68  		return nil, fmt.Errorf("failed to unmarshall the Flow config: %w", err)
    69  	}
    70  	return &flowConfig, nil
    71  }
    72  
    73  // RawViperConfig returns the raw viper config store.
    74  // Returns:
    75  //
    76  //	*viper.Viper: the raw viper config store.
    77  func RawViperConfig() *viper.Viper {
    78  	return conf
    79  }
    80  
    81  // BindPFlags binds the configuration to the cli pflag set. This should be called
    82  // after all pflags have been parsed. If the --config-file flag has been set the config will
    83  // be loaded from the specified config file.
    84  // Args:
    85  //
    86  //	c: The Flow configuration that will be used to unmarshall the configuration values into after binding pflags.
    87  //	This needs to be done because pflags may override a configuration value.
    88  //
    89  // Returns:
    90  //
    91  //	error: if there is any error encountered binding pflags or unmarshalling the config struct, all errors are considered irrecoverable.
    92  //	bool: true if --config-file flag was set and config file was loaded, false otherwise.
    93  //
    94  // Note: As configuration management is improved, this func should accept the entire Flow config as the arg to unmarshall new config values into.
    95  func BindPFlags(c *FlowConfig, flags *pflag.FlagSet) (bool, error) {
    96  	if !flags.Parsed() {
    97  		return false, errPflagsNotParsed
    98  	}
    99  
   100  	// update the config store values from config file if --config-file flag is set
   101  	// if config file provided we will use values from the file and skip binding pflags
   102  	overridden, err := overrideConfigFile(flags)
   103  	if err != nil {
   104  		return false, err
   105  	}
   106  
   107  	if !overridden {
   108  		err = conf.BindPFlags(flags)
   109  		if err != nil {
   110  			return false, fmt.Errorf("failed to bind pflag set: %w", err)
   111  		}
   112  		setAliases()
   113  	}
   114  
   115  	err = Unmarshall(c)
   116  	if err != nil {
   117  		return false, fmt.Errorf("failed to unmarshall the Flow config: %w", err)
   118  	}
   119  
   120  	return overridden, nil
   121  }
   122  
   123  // Unmarshall unmarshalls the Flow configuration into the provided FlowConfig struct.
   124  // Args:
   125  //
   126  //	flowConfig: the flow config struct used for unmarshalling.
   127  //
   128  // Returns:
   129  //
   130  //	error: if there is any error encountered unmarshalling the configuration, all errors are considered irrecoverable.
   131  func Unmarshall(flowConfig *FlowConfig) error {
   132  	err := conf.Unmarshal(flowConfig, func(decoderConfig *mapstructure.DecoderConfig) {
   133  		// enforce all fields are set on the FlowConfig struct
   134  		decoderConfig.ErrorUnset = true
   135  		// currently the entire flow configuration has not been moved to this package
   136  		// for now we allow key's in the config which are unused.
   137  		decoderConfig.ErrorUnused = false
   138  	})
   139  	if err != nil {
   140  		return fmt.Errorf("failed to unmarshal network config: %w", err)
   141  	}
   142  	return nil
   143  }
   144  
   145  // LogConfig logs configuration keys and values if they were overridden with a config file.
   146  // It also returns a map of keys for which the values were set by a config file.
   147  //
   148  // Parameters:
   149  //   - logger: *zerolog.Event to which the configuration keys and values will be logged.
   150  //   - flags: *pflag.FlagSet containing the set flags.
   151  //
   152  // Returns:
   153  //   - map[string]struct{}: map of keys for which the values were set by a config file.
   154  func LogConfig(logger *zerolog.Event, flags *pflag.FlagSet) map[string]struct{} {
   155  	keysToAvoid := make(map[string]struct{})
   156  
   157  	if flags.Lookup(configFileFlagName).Changed {
   158  		for _, key := range conf.AllKeys() {
   159  			logger.Str(key, fmt.Sprint(conf.Get(key)))
   160  			parts := strings.Split(key, ".")
   161  			if len(parts) == 2 {
   162  				keysToAvoid[parts[1]] = struct{}{}
   163  			} else {
   164  				keysToAvoid[key] = struct{}{}
   165  			}
   166  		}
   167  	}
   168  
   169  	return keysToAvoid
   170  }
   171  
   172  // setAliases sets aliases for config sub packages. This should be done directly after pflags are bound to the configuration store.
   173  // Upon initialization the conf will be loaded with the default config values, those values are then used as the default values for
   174  // all the CLI flags, the CLI flags are then bound to the configuration store and at this point all aliases should be set if configuration
   175  // keys do not match the CLI flags 1:1. ie: networking-connection-pruning -> network-config.networking-connection-pruning. After aliases
   176  // are set the conf store will override values with any CLI flag values that are set as expected.
   177  func setAliases() {
   178  	err := netconf.SetAliases(conf)
   179  	if err != nil {
   180  		panic(fmt.Errorf("failed to set network aliases: %w", err))
   181  	}
   182  }
   183  
   184  // overrideConfigFile overrides the default config file by reading in the config file at the path set
   185  // by the --config-file flag in our viper config store.
   186  //
   187  // Returns:
   188  //
   189  //	error: if there is any error encountered while reading new config file, all errors are considered irrecoverable.
   190  //	bool: true if the config was overridden by the new config file, false otherwise or if an error is encountered reading the new config file.
   191  func overrideConfigFile(flags *pflag.FlagSet) (bool, error) {
   192  	configFileFlag := flags.Lookup(configFileFlagName)
   193  	if configFileFlag.Changed {
   194  		p := configFileFlag.Value.String()
   195  		dirPath, fileName := splitConfigPath(p)
   196  		conf.AddConfigPath(dirPath)
   197  		conf.SetConfigName(fileName)
   198  		err := conf.ReadInConfig()
   199  		if err != nil {
   200  			return false, fmt.Errorf("failed to read config file %s: %w", p, err)
   201  		}
   202  		if len(conf.AllKeys()) == 0 {
   203  			return false, fmt.Errorf("failed to read in config file no config values found")
   204  		}
   205  		return true, nil
   206  	}
   207  	return false, nil
   208  }
   209  
   210  // splitConfigPath returns the directory and base name (without extension) of the config file from the provided path string.
   211  // If the file name does not match the expected pattern, the function panics.
   212  //
   213  // The expected pattern for file names is that they must consist of alphanumeric characters, hyphens, or underscores,
   214  // followed by a single dot and then the extension.
   215  //
   216  // Legitimate Inputs:
   217  //   - /path/to/my_config.yaml
   218  //   - /path/to/my-config123.yaml
   219  //   - my-config.yaml (when in the current directory)
   220  //
   221  // Illegitimate Inputs:
   222  //   - /path/to/my.config.yaml (contains multiple dots)
   223  //   - /path/to/my config.yaml (contains spaces)
   224  //   - /path/to/.config.yaml (does not have a file name before the dot)
   225  //
   226  // Args:
   227  //   - path: The file path string to be split into directory and base name.
   228  //
   229  // Returns:
   230  //   - The directory and base name without extension.
   231  //
   232  // Panics:
   233  //   - If the file name does not match the expected pattern.
   234  func splitConfigPath(path string) (string, string) {
   235  	// Regex to match filenames like 'my_config.yaml' or 'my-config.yaml' but not 'my.config.yaml'
   236  	validFileNamePattern := regexp.MustCompile(`^[a-zA-Z0-9_-]+\.[a-zA-Z0-9]+$`)
   237  
   238  	dir, name := filepath.Split(path)
   239  
   240  	// Panic if the file name does not match the expected pattern
   241  	if !validFileNamePattern.MatchString(name) {
   242  		panic(fmt.Errorf("Invalid config file name '%s'. Expected pattern: alphanumeric, hyphens, or underscores followed by a single dot and extension", name))
   243  	}
   244  
   245  	// Extracting the base name without extension
   246  	baseName := strings.Split(name, ".")[0]
   247  	return dir, baseName
   248  }
   249  
   250  func initialize() {
   251  	buf := bytes.NewBufferString(configFile)
   252  	conf.SetConfigType("yaml")
   253  	if err := conf.ReadConfig(buf); err != nil {
   254  		panic(fmt.Errorf("failed to initialize flow config failed to read in config file: %w", err))
   255  	}
   256  
   257  	// create validator, at this point you can register custom validation funcs
   258  	// struct tag translation etc.
   259  	validate = validator.New()
   260  }