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  }