github.com/clusterize-io/tusk@v0.6.3-0.20211001020217-cfe8a8cd0d4a/runner/option.go (about)

     1  package runner
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"strings"
     8  
     9  	"github.com/clusterize-io/tusk/marshal"
    10  	yaml "gopkg.in/yaml.v2"
    11  )
    12  
    13  // Option represents an abstract command line option.
    14  type Option struct {
    15  	ValueWithList `yaml:",inline"`
    16  
    17  	Short    string
    18  	Type     string
    19  	Usage    string
    20  	Private  bool
    21  	Required bool
    22  
    23  	// Used to determine value
    24  	Environment   string
    25  	DefaultValues ValueList `yaml:"default"`
    26  
    27  	// Computed members not specified in yaml file
    28  	Name       string `yaml:"-"`
    29  	Passed     string `yaml:"-"`
    30  	cacheValue string `yaml:"-"`
    31  	isCacheSet bool   `yaml:"-"`
    32  }
    33  
    34  // Dependencies returns a list of options that are required explicitly.
    35  // This does not include interpolations.
    36  func (o *Option) Dependencies() []string {
    37  	options := make([]string, 0, len(o.DefaultValues))
    38  	for _, value := range o.DefaultValues {
    39  		options = append(options, value.When.Dependencies()...)
    40  	}
    41  
    42  	return options
    43  }
    44  
    45  // UnmarshalYAML ensures that the option definition is valid.
    46  func (o *Option) UnmarshalYAML(unmarshal func(interface{}) error) error {
    47  	type optionType Option // Use new type to avoid recursion
    48  	if err := unmarshal((*optionType)(o)); err != nil {
    49  		return err
    50  	}
    51  
    52  	if len(o.Short) > 1 {
    53  		return fmt.Errorf(
    54  			"option short name %q cannot exceed one character",
    55  			o.Short,
    56  		)
    57  	}
    58  
    59  	if o.Private {
    60  		if o.Required {
    61  			return errors.New("option cannot be both private and required")
    62  		}
    63  
    64  		if o.Environment != "" {
    65  			return fmt.Errorf(
    66  				"environment variable %q defined for private option",
    67  				o.Environment,
    68  			)
    69  		}
    70  
    71  		if len(o.ValuesAllowed) != 0 {
    72  			return errors.New("option cannot be private and specify values")
    73  		}
    74  	}
    75  
    76  	if o.Required && len(o.DefaultValues) > 0 {
    77  		return errors.New("default value defined for required option")
    78  	}
    79  
    80  	return nil
    81  }
    82  
    83  // Evaluate determines an option's value.
    84  //
    85  // The order of priority is:
    86  //   1. Command-line option passed
    87  //   2. Environment variable set
    88  //   3. The first item in the default value list with a valid when clause
    89  //
    90  // Values may also be cached to avoid re-running commands.
    91  func (o *Option) Evaluate(ctx Context, vars map[string]string) (string, error) {
    92  	if o == nil {
    93  		return "", nil
    94  	}
    95  
    96  	value, err := o.getValue(ctx, vars)
    97  	if err != nil {
    98  		return "", err
    99  	}
   100  
   101  	o.cache(value)
   102  
   103  	return value, nil
   104  }
   105  
   106  func (o *Option) getValue(ctx Context, vars map[string]string) (string, error) {
   107  	if o.isCacheSet {
   108  		return o.cacheValue, nil
   109  	}
   110  
   111  	if !o.Private {
   112  		if value, found := o.getSpecified(); found {
   113  			if err := o.validateSpecified(value, "option "+o.Name); err != nil {
   114  				return "", err
   115  			}
   116  
   117  			return value, nil
   118  		}
   119  	}
   120  
   121  	if o.Required {
   122  		return "", fmt.Errorf("no value passed for required option: %s", o.Name)
   123  	}
   124  
   125  	return o.getDefaultValue(ctx, vars)
   126  }
   127  
   128  func (o *Option) getSpecified() (value string, found bool) {
   129  	if o.Passed != "" {
   130  		return o.Passed, true
   131  	}
   132  
   133  	envValue := os.Getenv(o.Environment)
   134  	if envValue != "" {
   135  		return envValue, true
   136  	}
   137  
   138  	return "", false
   139  }
   140  
   141  func (o *Option) getDefaultValue(ctx Context, vars map[string]string) (string, error) {
   142  	for _, candidate := range o.DefaultValues {
   143  		if err := candidate.When.Validate(ctx, vars); err != nil {
   144  			if !IsFailedCondition(err) {
   145  				return "", err
   146  			}
   147  			continue
   148  		}
   149  
   150  		value, err := candidate.commandValueOrDefault(ctx)
   151  		if err != nil {
   152  			return "", fmt.Errorf("could not compute value for option %q: %w", o.Name, err)
   153  		}
   154  
   155  		return value, nil
   156  	}
   157  
   158  	if o.isNumeric() {
   159  		return "0", nil
   160  	}
   161  
   162  	if o.isBoolean() {
   163  		return "false", nil
   164  	}
   165  
   166  	return "", nil
   167  }
   168  
   169  func (o *Option) cache(value string) {
   170  	o.isCacheSet = true
   171  	o.cacheValue = value
   172  }
   173  
   174  func (o *Option) isNumeric() bool {
   175  	switch strings.ToLower(o.Type) {
   176  	case "int", "integer", "float", "float64", "double":
   177  		return true
   178  	default:
   179  		return false
   180  	}
   181  }
   182  
   183  func (o *Option) isBoolean() bool {
   184  	switch strings.ToLower(o.Type) {
   185  	case "bool", "boolean":
   186  		return true
   187  	default:
   188  		return false
   189  	}
   190  }
   191  
   192  // Options represents an ordered set of options as specified in the config.
   193  type Options []*Option
   194  
   195  // UnmarshalYAML unmarshals an ordered set of options and assigns names.
   196  func (o *Options) UnmarshalYAML(unmarshal func(interface{}) error) error {
   197  	var ms yaml.MapSlice
   198  	if err := unmarshal(&ms); err != nil {
   199  		return err
   200  	}
   201  
   202  	options, err := getOptionsWithOrder(ms)
   203  	if err != nil {
   204  		return err
   205  	}
   206  
   207  	*o = options
   208  
   209  	return nil
   210  }
   211  
   212  // Lookup finds an Option by name.
   213  func (o *Options) Lookup(name string) (*Option, bool) {
   214  	for _, opt := range *o {
   215  		if opt.Name == name {
   216  			return opt, true
   217  		}
   218  	}
   219  
   220  	return nil, false
   221  }
   222  
   223  // getOptionsWithOrder returns both the option map and the ordered names.
   224  func getOptionsWithOrder(ms yaml.MapSlice) ([]*Option, error) {
   225  	options := make([]*Option, 0, len(ms))
   226  	assign := func(name string, text []byte) error {
   227  		var opt Option
   228  		if err := yaml.UnmarshalStrict(text, &opt); err != nil {
   229  			return err
   230  		}
   231  		opt.Name = name
   232  
   233  		options = append(options, &opt)
   234  
   235  		return nil
   236  	}
   237  
   238  	_, err := marshal.ParseOrderedMap(ms, assign)
   239  	return options, err
   240  }