github.com/discordapp/buildkite-agent@v2.6.6+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/utils" 12 "github.com/codegangsta/cli" 13 "github.com/oleiade/reflections" 14 ) 15 16 type Loader struct { 17 // The context that is passed when using a codegangsta/cli action 18 CLI *cli.Context 19 20 // The struct that the config values will be loaded into 21 Config interface{} 22 23 // A slice of paths to files that should be used as config files 24 DefaultConfigFilePaths []string 25 26 // The file that was used when loading this configuration 27 File *File 28 } 29 30 var CLISpecialNameRegex = regexp.MustCompile(`(arg):(\d+)`) 31 32 // A shortcut for loading a config from the CLI 33 func Load(c *cli.Context, cfg interface{}) error { 34 l := Loader{CLI: c, Config: cfg} 35 36 return l.Load() 37 } 38 39 // Loads the config from the CLI and config files that are present. 40 func (l *Loader) Load() error { 41 // Try and find a config file, either passed in the command line using 42 // --config, or in one of the default configuration file paths. 43 if l.CLI.String("config") != "" { 44 file := File{Path: l.CLI.String("config")} 45 46 // Because this file was passed in manually, we should throw an error 47 // if it doesn't exist. 48 if file.Exists() { 49 l.File = &file 50 } else { 51 return fmt.Errorf("A configuration file could not be found at: %s", file.AbsolutePath()) 52 } 53 } else if len(l.DefaultConfigFilePaths) > 0 { 54 for _, path := range l.DefaultConfigFilePaths { 55 file := File{Path: path} 56 57 // If the config file exists, save it to the loader and 58 // don't bother checking the others. 59 if file.Exists() { 60 l.File = &file 61 break 62 } 63 } 64 } 65 66 // If a file was found, then we should load it 67 if l.File != nil { 68 // Attempt to load the config file we've found 69 if err := l.File.Load(); err != nil { 70 return err 71 } 72 } 73 74 // Now it's onto actually setting the fields. We start by getting all 75 // the fields from the configuration interface 76 var fields []string 77 fields, _ = reflections.Fields(l.Config) 78 79 // Loop through each of the fields, and look for tags and handle them 80 // appropriately 81 for _, fieldName := range fields { 82 // Start by loading the value from the CLI context if the tag 83 // exists 84 cliName, _ := reflections.GetFieldTag(l.Config, fieldName, "cli") 85 if cliName != "" { 86 // Load the value from the CLI Context 87 err := l.setFieldValueFromCLI(fieldName, cliName) 88 if err != nil { 89 return err 90 } 91 } 92 93 // Are there any normalizations we need to make? 94 normalization, _ := reflections.GetFieldTag(l.Config, fieldName, "normalize") 95 if normalization != "" { 96 // Apply the normalization 97 err := l.normalizeField(fieldName, normalization) 98 if err != nil { 99 return err 100 } 101 } 102 103 // Check for field deprecation 104 deprecationError, _ := reflections.GetFieldTag(l.Config, fieldName, "deprecated") 105 if deprecationError != "" { 106 // If the deprecated field's value isn't emtpy, then we 107 // return the deprecation error message. 108 if !l.fieldValueIsEmpty(fieldName) { 109 return fmt.Errorf(deprecationError) 110 } 111 } 112 113 // Perform validations 114 validationRules, _ := reflections.GetFieldTag(l.Config, fieldName, "validate") 115 if validationRules != "" { 116 // Determine the label for the field 117 label, _ := reflections.GetFieldTag(l.Config, fieldName, "label") 118 if label == "" { 119 // Use the cli name if it exists, but if it 120 // doesn't, just default to the structs field 121 // name. Not great, but works! 122 if cliName != "" { 123 label = cliName 124 } else { 125 label = fieldName 126 } 127 } 128 129 // Validate the fieid, and if it fails, return it's 130 // error. 131 err := l.validateField(fieldName, label, validationRules) 132 if err != nil { 133 return err 134 } 135 } 136 } 137 138 return nil 139 } 140 141 func (l Loader) setFieldValueFromCLI(fieldName string, cliName string) error { 142 // Get the kind of field we need to set 143 fieldKind, err := reflections.GetFieldKind(l.Config, fieldName) 144 if err != nil { 145 return fmt.Errorf(`Failed to get the type of struct field %s`, fieldName) 146 } 147 148 var value interface{} 149 150 // See the if the cli option is using the special format i.e. (arg:1) 151 special := CLISpecialNameRegex.FindStringSubmatch(cliName) 152 if len(special) == 3 { 153 // Should this cli option be loaded from the CLI arguments? 154 if special[1] == "arg" { 155 // Convert the arg position to an integer 156 i, err := strconv.Atoi(special[2]) 157 if err != nil { 158 return fmt.Errorf("Failed to convert string to int: %s", err) 159 } 160 161 // Only set the value if the args are long enough for 162 // the position to exist. 163 if len(l.CLI.Args()) > i { 164 // Get the value from the args 165 value = l.CLI.Args()[i] 166 } 167 } 168 } else { 169 // If the cli name didn't have the special format, then we need to 170 // either load from the context's flags, or from a config file. 171 172 // We start by defaulting the value to what ever was provided 173 // by the configuration file 174 if l.File != nil { 175 if configFileValue, ok := l.File.Config[cliName]; ok { 176 // Convert the config file value to it's correct type 177 if fieldKind == reflect.String { 178 value = configFileValue 179 } else if fieldKind == reflect.Slice { 180 value = strings.Split(configFileValue, ",") 181 } else if fieldKind == reflect.Bool { 182 value, _ = strconv.ParseBool(configFileValue) 183 } else { 184 return fmt.Errorf("Unable to convert string to type %s", fieldKind) 185 } 186 } 187 } 188 189 // If a value hasn't been found in a config file, but there 190 // _is_ one provided by the CLI context, then use that. 191 if value == nil || l.cliValueIsSet(cliName) { 192 if fieldKind == reflect.String { 193 value = l.CLI.String(cliName) 194 } else if fieldKind == reflect.Slice { 195 value = l.CLI.StringSlice(cliName) 196 } else if fieldKind == reflect.Bool { 197 value = l.CLI.Bool(cliName) 198 } else { 199 return fmt.Errorf("Unable to handle type: %s", fieldKind) 200 } 201 } 202 } 203 204 // Set the value to the cfg 205 if value != nil { 206 err = reflections.SetField(l.Config, fieldName, value) 207 if err != nil { 208 return fmt.Errorf("Could not set value `%s` to field `%s` (%s)", value, fieldName, err) 209 } 210 } 211 212 return nil 213 } 214 215 func (l Loader) Errorf(format string, v ...interface{}) error { 216 suffix := fmt.Sprintf(" See: `%s %s --help`", l.CLI.App.Name, l.CLI.Command.Name) 217 218 return fmt.Errorf(format+suffix, v...) 219 } 220 221 func (l Loader) cliValueIsSet(cliName string) bool { 222 if l.CLI.IsSet(cliName) { 223 return true 224 } else { 225 // cli.Context#IsSet only checks to see if the command was set via the cli, not 226 // via the environment. So here we do some hacks to find out the name of the 227 // EnvVar, and return true if it was set. 228 for _, flag := range l.CLI.Command.Flags { 229 name, _ := reflections.GetField(flag, "Name") 230 envVar, _ := reflections.GetField(flag, "EnvVar") 231 if name == cliName && envVar != "" { 232 // Make sure envVar is a string 233 if envVarStr, ok := envVar.(string); ok { 234 envVarStr = strings.TrimSpace(string(envVarStr)) 235 236 return os.Getenv(envVarStr) != "" 237 } 238 } 239 } 240 } 241 242 return false 243 } 244 245 func (l Loader) fieldValueIsEmpty(fieldName string) bool { 246 // We need to use the field kind to determine the type of empty test. 247 value, _ := reflections.GetField(l.Config, fieldName) 248 fieldKind, _ := reflections.GetFieldKind(l.Config, fieldName) 249 250 if fieldKind == reflect.String { 251 return value == "" 252 } else if fieldKind == reflect.Slice { 253 v := reflect.ValueOf(value) 254 return v.Len() == 0 255 } else if fieldKind == reflect.Bool { 256 return value == false 257 } else { 258 panic(fmt.Sprintf("Can't determine empty-ness for field type %s", fieldKind)) 259 } 260 261 return false 262 } 263 264 func (l Loader) validateField(fieldName string, label string, validationRules string) error { 265 // Split up the validation rules 266 rules := strings.Split(validationRules, ",") 267 268 // Loop through each rule, and perform it 269 for _, rule := range rules { 270 if rule == "required" { 271 if l.fieldValueIsEmpty(fieldName) { 272 return l.Errorf("Missing %s.", label) 273 } 274 } else if rule == "file-exists" { 275 value, _ := reflections.GetField(l.Config, fieldName) 276 277 // Make sure the value is converted to a string 278 if valueAsString, ok := value.(string); ok { 279 // Return an error if the path doesn't exist 280 if _, err := os.Stat(valueAsString); err != nil { 281 return fmt.Errorf("Could not find %s located at %s", label, value) 282 } 283 } 284 } else { 285 return fmt.Errorf("Unknown config validation rule `%s`", rule) 286 } 287 } 288 289 return nil 290 } 291 292 func (l Loader) normalizeField(fieldName string, normalization string) error { 293 if normalization == "filepath" { 294 value, _ := reflections.GetField(l.Config, fieldName) 295 fieldKind, _ := reflections.GetFieldKind(l.Config, fieldName) 296 297 // Make sure we're normalizing a string filed 298 if fieldKind != reflect.String { 299 return fmt.Errorf("filepath normalization only works on string fields") 300 } 301 302 // Normalize the field to be a filepath 303 if valueAsString, ok := value.(string); ok { 304 normalizedPath := utils.NormalizeFilePath(valueAsString) 305 if err := reflections.SetField(l.Config, fieldName, normalizedPath); err != nil { 306 return err 307 } 308 } 309 } else { 310 return fmt.Errorf("Unknown normalization `%s`", normalization) 311 } 312 313 return nil 314 }