github.com/bshelton229/agent@v3.5.4+incompatible/cliconfig/loader.go (about) 1 package cliconfig 2 3 import ( 4 "fmt" 5 "os" 6 "reflect" 7 "regexp" 8 "strconv" 9 "strings" 10 11 "github.com/buildkite/agent/logger" 12 "github.com/buildkite/agent/utils" 13 "github.com/oleiade/reflections" 14 "github.com/urfave/cli" 15 ) 16 17 type Loader struct { 18 // The context that is passed when using a codegangsta/cli action 19 CLI *cli.Context 20 21 // The struct that the config values will be loaded into 22 Config interface{} 23 24 // A slice of paths to files that should be used as config files 25 DefaultConfigFilePaths []string 26 27 // The file that was used when loading this configuration 28 File *File 29 } 30 31 var argCliNameRegexp = regexp.MustCompile(`arg:(\d+)`) 32 33 // A shortcut for loading a config from the CLI 34 func Load(c *cli.Context, cfg interface{}) error { 35 l := Loader{CLI: c, Config: cfg} 36 37 return l.Load() 38 } 39 40 // Loads the config from the CLI and config files that are present. 41 func (l *Loader) Load() error { 42 // Try and find a config file, either passed in the command line using 43 // --config, or in one of the default configuration file paths. 44 if l.CLI.String("config") != "" { 45 file := File{Path: l.CLI.String("config")} 46 47 // Because this file was passed in manually, we should throw an error 48 // if it doesn't exist. 49 if file.Exists() { 50 l.File = &file 51 } else { 52 absolutePath, _ := file.AbsolutePath() 53 return fmt.Errorf("A configuration file could not be found at: %q", absolutePath) 54 } 55 } else if len(l.DefaultConfigFilePaths) > 0 { 56 for _, path := range l.DefaultConfigFilePaths { 57 file := File{Path: path} 58 59 // If the config file exists, save it to the loader and 60 // don't bother checking the others. 61 if file.Exists() { 62 l.File = &file 63 break 64 } 65 } 66 } 67 68 // If a file was found, then we should load it 69 if l.File != nil { 70 // Attempt to load the config file we've found 71 if err := l.File.Load(); err != nil { 72 return err 73 } 74 } 75 76 // Now it's onto actually setting the fields. We start by getting all 77 // the fields from the configuration interface 78 var fields []string 79 fields, _ = reflections.Fields(l.Config) 80 81 // Loop through each of the fields, and look for tags and handle them 82 // appropriately 83 for _, fieldName := range fields { 84 // Start by loading the value from the CLI context if the tag 85 // exists 86 cliName, _ := reflections.GetFieldTag(l.Config, fieldName, "cli") 87 if cliName != "" { 88 // Load the value from the CLI Context 89 err := l.setFieldValueFromCLI(fieldName, cliName) 90 if err != nil { 91 return err 92 } 93 } 94 95 // Are there any normalizations we need to make? 96 normalization, _ := reflections.GetFieldTag(l.Config, fieldName, "normalize") 97 if normalization != "" { 98 // Apply the normalization 99 err := l.normalizeField(fieldName, normalization) 100 if err != nil { 101 return err 102 } 103 } 104 105 // Check for field rename deprecations 106 renamedToFieldName, _ := reflections.GetFieldTag(l.Config, fieldName, "deprecated-and-renamed-to") 107 if renamedToFieldName != "" { 108 // If the deprecated field's value isn't empty, then we 109 // log a message, and set the proper config for them. 110 if !l.fieldValueIsEmpty(fieldName) { 111 renamedFieldCliName, _ := reflections.GetFieldTag(l.Config, renamedToFieldName, "cli") 112 if renamedFieldCliName != "" { 113 logger.Warn("The config option `%s` has been renamed to `%s`. Please update your configuration.", cliName, renamedFieldCliName) 114 } 115 116 value, _ := reflections.GetField(l.Config, fieldName) 117 118 // Error if they specify the deprecated version and the new version 119 if !l.fieldValueIsEmpty(renamedToFieldName) { 120 renamedFieldValue, _ := reflections.GetField(l.Config, renamedToFieldName) 121 return fmt.Errorf("Can't set config option `%s=%v` because `%s=%v` has already been set", cliName, value, renamedFieldCliName, renamedFieldValue) 122 } 123 124 // Set the proper config based on the deprecated value 125 if value != nil { 126 err := reflections.SetField(l.Config, renamedToFieldName, value) 127 if err != nil { 128 return fmt.Errorf("Could not set value `%s` to field `%s` (%s)", value, renamedToFieldName, err) 129 } 130 } 131 } 132 } 133 134 // Check for field deprecation 135 deprecationError, _ := reflections.GetFieldTag(l.Config, fieldName, "deprecated") 136 if deprecationError != "" { 137 // If the deprecated field's value isn't empty, then we 138 // return the deprecation error message. 139 if !l.fieldValueIsEmpty(fieldName) { 140 return fmt.Errorf(deprecationError) 141 } 142 } 143 144 // Perform validations 145 validationRules, _ := reflections.GetFieldTag(l.Config, fieldName, "validate") 146 if validationRules != "" { 147 // Determine the label for the field 148 label, _ := reflections.GetFieldTag(l.Config, fieldName, "label") 149 if label == "" { 150 // Use the cli name if it exists, but if it 151 // doesn't, just default to the structs field 152 // name. Not great, but works! 153 if cliName != "" { 154 label = cliName 155 } else { 156 label = fieldName 157 } 158 } 159 160 // Validate the fieid, and if it fails, return it's 161 // error. 162 err := l.validateField(fieldName, label, validationRules) 163 if err != nil { 164 return err 165 } 166 } 167 } 168 169 return nil 170 } 171 172 func (l Loader) setFieldValueFromCLI(fieldName string, cliName string) error { 173 // Get the kind of field we need to set 174 fieldKind, err := reflections.GetFieldKind(l.Config, fieldName) 175 if err != nil { 176 return fmt.Errorf(`Failed to get the type of struct field %s`, fieldName) 177 } 178 179 var value interface{} 180 181 // See the if the cli option is using the arg format i.e. (arg:1) 182 argMatch := argCliNameRegexp.FindStringSubmatch(cliName) 183 if len(argMatch) > 0 { 184 argNum := argMatch[1] 185 186 // Convert the arg position to an integer 187 argIndex, err := strconv.Atoi(argNum) 188 if err != nil { 189 return fmt.Errorf("Failed to convert string to int: %s", err) 190 } 191 192 // Only set the value if the args are long enough for 193 // the position to exist. 194 if len(l.CLI.Args()) > argIndex { 195 value = l.CLI.Args()[argIndex] 196 } 197 198 // Otherwise see if we can pull it from an environment variable 199 // (and fail gracefuly if we can't) 200 if value == nil { 201 envName, err := reflections.GetFieldTag(l.Config, fieldName, "env") 202 if err == nil { 203 if envValue, envSet := os.LookupEnv(envName); envSet { 204 value = envValue 205 } 206 } 207 } 208 } else { 209 // If the cli name didn't have the special format, then we need to 210 // either load from the context's flags, or from a config file. 211 212 // We start by defaulting the value to what ever was provided 213 // by the configuration file 214 if l.File != nil { 215 if configFileValue, ok := l.File.Config[cliName]; ok { 216 // Convert the config file value to it's correct type 217 if fieldKind == reflect.String { 218 value = configFileValue 219 } else if fieldKind == reflect.Slice { 220 value = strings.Split(configFileValue, ",") 221 } else if fieldKind == reflect.Bool { 222 value, _ = strconv.ParseBool(configFileValue) 223 } else if fieldKind == reflect.Int { 224 value, _ = strconv.Atoi(configFileValue) 225 } else { 226 return fmt.Errorf("Unable to convert string to type %s", fieldKind) 227 } 228 } 229 } 230 231 // If a value hasn't been found in a config file, but there 232 // _is_ one provided by the CLI context, then use that. 233 if value == nil || l.cliValueIsSet(cliName) { 234 if fieldKind == reflect.String { 235 value = l.CLI.String(cliName) 236 } else if fieldKind == reflect.Slice { 237 value = l.CLI.StringSlice(cliName) 238 } else if fieldKind == reflect.Bool { 239 value = l.CLI.Bool(cliName) 240 } else if fieldKind == reflect.Int { 241 value = l.CLI.Int(cliName) 242 } else { 243 return fmt.Errorf("Unable to handle type: %s", fieldKind) 244 } 245 } 246 } 247 248 // Set the value to the cfg 249 if value != nil { 250 err = reflections.SetField(l.Config, fieldName, value) 251 if err != nil { 252 return fmt.Errorf("Could not set value `%s` to field `%s` (%s)", value, fieldName, err) 253 } 254 } 255 256 return nil 257 } 258 259 func (l Loader) Errorf(format string, v ...interface{}) error { 260 suffix := fmt.Sprintf(" See: `%s %s --help`", l.CLI.App.Name, l.CLI.Command.Name) 261 262 return fmt.Errorf(format+suffix, v...) 263 } 264 265 func (l Loader) cliValueIsSet(cliName string) bool { 266 if l.CLI.IsSet(cliName) { 267 return true 268 } else { 269 // cli.Context#IsSet only checks to see if the command was set via the cli, not 270 // via the environment. So here we do some hacks to find out the name of the 271 // EnvVar, and return true if it was set. 272 for _, flag := range l.CLI.Command.Flags { 273 name, _ := reflections.GetField(flag, "Name") 274 envVar, _ := reflections.GetField(flag, "EnvVar") 275 if name == cliName && envVar != "" { 276 // Make sure envVar is a string 277 if envVarStr, ok := envVar.(string); ok { 278 envVarStr = strings.TrimSpace(string(envVarStr)) 279 280 return os.Getenv(envVarStr) != "" 281 } 282 } 283 } 284 } 285 286 return false 287 } 288 289 func (l Loader) fieldValueIsEmpty(fieldName string) bool { 290 // We need to use the field kind to determine the type of empty test. 291 value, _ := reflections.GetField(l.Config, fieldName) 292 fieldKind, _ := reflections.GetFieldKind(l.Config, fieldName) 293 294 if fieldKind == reflect.String { 295 return value == "" 296 } else if fieldKind == reflect.Slice { 297 v := reflect.ValueOf(value) 298 return v.Len() == 0 299 } else if fieldKind == reflect.Bool { 300 return value == false 301 } else { 302 panic(fmt.Sprintf("Can't determine empty-ness for field type %s", fieldKind)) 303 } 304 305 return false 306 } 307 308 func (l Loader) validateField(fieldName string, label string, validationRules string) error { 309 // Split up the validation rules 310 rules := strings.Split(validationRules, ",") 311 312 // Loop through each rule, and perform it 313 for _, rule := range rules { 314 if rule == "required" { 315 if l.fieldValueIsEmpty(fieldName) { 316 return l.Errorf("Missing %s.", label) 317 } 318 } else if rule == "file-exists" { 319 value, _ := reflections.GetField(l.Config, fieldName) 320 321 // Make sure the value is converted to a string 322 if valueAsString, ok := value.(string); ok { 323 // Return an error if the path doesn't exist 324 if _, err := os.Stat(valueAsString); err != nil { 325 return fmt.Errorf("Could not find %s located at %s", label, value) 326 } 327 } 328 } else { 329 return fmt.Errorf("Unknown config validation rule `%s`", rule) 330 } 331 } 332 333 return nil 334 } 335 336 func (l Loader) normalizeField(fieldName string, normalization string) error { 337 if normalization == "filepath" { 338 value, _ := reflections.GetField(l.Config, fieldName) 339 fieldKind, _ := reflections.GetFieldKind(l.Config, fieldName) 340 341 // Make sure we're normalizing a string filed 342 if fieldKind != reflect.String { 343 return fmt.Errorf("filepath normalization only works on string fields") 344 } 345 346 // Normalize the field to be a filepath 347 if valueAsString, ok := value.(string); ok { 348 normalizedPath, err := utils.NormalizeFilePath(valueAsString) 349 if err != nil { 350 return err 351 } 352 353 if err := reflections.SetField(l.Config, fieldName, normalizedPath); err != nil { 354 return err 355 } 356 } 357 } else if normalization == "commandpath" { 358 value, _ := reflections.GetField(l.Config, fieldName) 359 fieldKind, _ := reflections.GetFieldKind(l.Config, fieldName) 360 361 // Make sure we're normalizing a string filed 362 if fieldKind != reflect.String { 363 return fmt.Errorf("commandpath normalization only works on string fields") 364 } 365 366 // Normalize the field to be a command 367 if valueAsString, ok := value.(string); ok { 368 normalizedCommandPath, err := utils.NormalizeCommand(valueAsString) 369 if err != nil { 370 return err 371 } 372 373 if err := reflections.SetField(l.Config, fieldName, normalizedCommandPath); err != nil { 374 return err 375 } 376 } 377 } else if normalization == "list" { 378 value, _ := reflections.GetField(l.Config, fieldName) 379 fieldKind, _ := reflections.GetFieldKind(l.Config, fieldName) 380 381 // Make sure we're normalizing a string filed 382 if fieldKind != reflect.Slice { 383 return fmt.Errorf("list normalization only works on slice fields") 384 } 385 386 // Normalize the field to be a command 387 if valueAsSlice, ok := value.([]string); ok { 388 normalizedSlice := []string{} 389 390 for _, value := range valueAsSlice { 391 // Split values with commas into fields 392 for _, normalized := range strings.Split(value, ",") { 393 normalizedSlice = append(normalizedSlice, normalized) 394 } 395 } 396 397 if err := reflections.SetField(l.Config, fieldName, normalizedSlice); err != nil { 398 return err 399 } 400 } 401 402 } else { 403 return fmt.Errorf("Unknown normalization `%s`", normalization) 404 } 405 406 return nil 407 }