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  }