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 }