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

     1  package runner
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"runtime"
     7  	"strings"
     8  
     9  	"github.com/clusterize-io/tusk/marshal"
    10  	yaml "gopkg.in/yaml.v2"
    11  )
    12  
    13  // When defines the conditions for running a task.
    14  type When struct {
    15  	Command   marshal.StringList `yaml:",omitempty"`
    16  	Exists    marshal.StringList `yaml:",omitempty"`
    17  	NotExists marshal.StringList `yaml:"not-exists,omitempty"`
    18  	OS        marshal.StringList `yaml:",omitempty"`
    19  
    20  	Environment map[string]marshal.NullableStringList `yaml:",omitempty"`
    21  	Equal       map[string]marshal.StringList         `yaml:",omitempty"`
    22  	NotEqual    map[string]marshal.StringList         `yaml:"not-equal,omitempty"`
    23  }
    24  
    25  // UnmarshalYAML warns about deprecated features.
    26  func (w *When) UnmarshalYAML(unmarshal func(interface{}) error) error {
    27  	var equal marshal.StringList
    28  	slCandidate := marshal.UnmarshalCandidate{
    29  		Unmarshal: func() error { return unmarshal(&equal) },
    30  		Assign: func() {
    31  			equalityMap := make(map[string]marshal.StringList, len(equal))
    32  			for _, key := range equal {
    33  				equalityMap[key] = marshal.StringList{"true"}
    34  			}
    35  			*w = When{Equal: equalityMap}
    36  		},
    37  	}
    38  
    39  	type whenType When // Use new type to avoid recursion
    40  	var whenItem whenType
    41  	var ms yaml.MapSlice
    42  	whenCandidate := marshal.UnmarshalCandidate{
    43  		Unmarshal: func() error {
    44  			if err := unmarshal(&whenItem); err != nil {
    45  				return err
    46  			}
    47  
    48  			if err := unmarshal(&ms); err != nil {
    49  				return err
    50  			}
    51  
    52  			return nil
    53  		},
    54  		Assign: func() {
    55  			*w = When(whenItem)
    56  			fixNilEnvironment(w, ms)
    57  		},
    58  	}
    59  
    60  	return marshal.UnmarshalOneOf(slCandidate, whenCandidate)
    61  }
    62  
    63  // fixNilEnvironment replaces a single nil specified in a yaml configuration as
    64  // a list of nil, which is the more logical interpretation of the value in this
    65  // situation.
    66  func fixNilEnvironment(w *When, ms yaml.MapSlice) {
    67  	for _, clauseMS := range ms {
    68  		if name, ok := clauseMS.Key.(string); !ok || name != "environment" {
    69  			continue
    70  		}
    71  
    72  		for _, envMS := range clauseMS.Value.(yaml.MapSlice) {
    73  			envVar := envMS.Key.(string)
    74  
    75  			if envMS.Value == nil {
    76  				w.Environment[envVar] = marshal.NullableStringList{nil}
    77  			}
    78  		}
    79  	}
    80  }
    81  
    82  // Dependencies returns a list of options that are required explicitly.
    83  // This does not include interpolations.
    84  func (w *When) Dependencies() []string {
    85  	if w == nil {
    86  		return nil
    87  	}
    88  
    89  	// Use a map to prevent duplicates
    90  	references := make(map[string]struct{})
    91  
    92  	for opt := range w.Equal {
    93  		references[opt] = struct{}{}
    94  	}
    95  	for opt := range w.NotEqual {
    96  		references[opt] = struct{}{}
    97  	}
    98  
    99  	options := make([]string, 0, len(references))
   100  	for opt := range references {
   101  		options = append(options, opt)
   102  	}
   103  
   104  	return options
   105  }
   106  
   107  // Validate returns an error if any when clauses fail.
   108  func (w *When) Validate(ctx Context, vars map[string]string) error {
   109  	if w == nil {
   110  		return nil
   111  	}
   112  
   113  	return validateAny(
   114  		w.validateOS(),
   115  		w.validateEqual(vars),
   116  		w.validateNotEqual(vars),
   117  		w.validateEnv(),
   118  		w.validateExists(),
   119  		w.validateNotExists(),
   120  		w.validateCommand(ctx),
   121  	)
   122  }
   123  
   124  // TODO: Should this be done in parallel?
   125  func validateAny(errs ...error) error {
   126  	var errOutput error
   127  	for _, err := range errs {
   128  		if err == nil {
   129  			return nil
   130  		}
   131  
   132  		if errOutput == nil && !IsUnspecifiedClause(err) {
   133  			errOutput = err
   134  		}
   135  	}
   136  
   137  	return errOutput
   138  }
   139  
   140  func (w *When) validateCommand(ctx Context) error {
   141  	if len(w.Command) == 0 {
   142  		return newUnspecifiedError("command")
   143  	}
   144  
   145  	for _, command := range w.Command {
   146  		if err := testCommand(ctx, command); err == nil {
   147  			return nil
   148  		}
   149  	}
   150  
   151  	return newCondFailErrorf("no commands exited successfully")
   152  }
   153  
   154  func (w *When) validateExists() error {
   155  	if len(w.Exists) == 0 {
   156  		return newUnspecifiedError("exists")
   157  	}
   158  
   159  	for _, f := range w.Exists {
   160  		if _, err := os.Stat(f); err != nil {
   161  			if !os.IsNotExist(err) {
   162  				return err
   163  			}
   164  			continue
   165  		}
   166  
   167  		return nil
   168  	}
   169  
   170  	return newCondFailErrorf("no required file existed: %s", w.Exists)
   171  }
   172  
   173  func (w *When) validateNotExists() error {
   174  	if len(w.NotExists) == 0 {
   175  		return newUnspecifiedError("not-exists")
   176  	}
   177  
   178  	for _, f := range w.NotExists {
   179  		if _, err := os.Stat(f); err != nil {
   180  			if os.IsNotExist(err) {
   181  				return nil
   182  			}
   183  			return err
   184  		}
   185  	}
   186  
   187  	return newCondFailErrorf("all files exist: %s", w.NotExists)
   188  }
   189  
   190  func (w *When) validateOS() error {
   191  	if len(w.OS) == 0 {
   192  		return newUnspecifiedError("os")
   193  	}
   194  
   195  	return validateOneOf(
   196  		"current OS", runtime.GOOS, w.OS,
   197  		func(expected, actual string) bool {
   198  			return normalizeOS(expected) == actual
   199  		},
   200  	)
   201  }
   202  
   203  func (w *When) validateEnv() error {
   204  	if len(w.Environment) == 0 {
   205  		return newUnspecifiedError("env")
   206  	}
   207  
   208  	for varName, values := range w.Environment {
   209  		stringValues := make([]string, 0, len(values))
   210  		for _, value := range values {
   211  			if value != nil {
   212  				stringValues = append(stringValues, *value)
   213  			}
   214  		}
   215  
   216  		isNullAllowed := len(values) != len(stringValues)
   217  
   218  		actual, ok := os.LookupEnv(varName)
   219  		if !ok {
   220  			if isNullAllowed {
   221  				return nil
   222  			}
   223  
   224  			continue
   225  		}
   226  
   227  		if err := validateOneOf(
   228  			fmt.Sprintf("environment variable %s", varName),
   229  			actual,
   230  			stringValues,
   231  			func(a, b string) bool { return a == b },
   232  		); err == nil {
   233  			return nil
   234  		}
   235  	}
   236  
   237  	return newCondFailError("no environment variables matched")
   238  }
   239  
   240  func (w *When) validateEqual(vars map[string]string) error {
   241  	if len(w.Equal) == 0 {
   242  		return newUnspecifiedError("equal")
   243  	}
   244  
   245  	return validateEquality(vars, w.Equal, func(a, b string) bool {
   246  		return a == b
   247  	})
   248  }
   249  
   250  func (w *When) validateNotEqual(vars map[string]string) error {
   251  	if len(w.NotEqual) == 0 {
   252  		return newUnspecifiedError("not-equal")
   253  	}
   254  
   255  	return validateEquality(vars, w.NotEqual, func(a, b string) bool {
   256  		return a != b
   257  	})
   258  }
   259  
   260  func validateOneOf(
   261  	desc, value string, required []string, compare func(string, string) bool,
   262  ) error {
   263  	for _, expected := range required {
   264  		if compare(expected, value) {
   265  			return nil
   266  		}
   267  	}
   268  
   269  	return newCondFailErrorf("%s (%s) not listed in %v", desc, value, required)
   270  }
   271  
   272  func normalizeOS(name string) string {
   273  	lower := strings.ToLower(name)
   274  
   275  	for _, alt := range []string{"mac", "macos", "osx"} {
   276  		if lower == alt {
   277  			return "darwin"
   278  		}
   279  	}
   280  
   281  	for _, alt := range []string{"win"} {
   282  		if lower == alt {
   283  			return "windows"
   284  		}
   285  	}
   286  
   287  	return lower
   288  }
   289  
   290  func testCommand(ctx Context, command string) error {
   291  	cmd := newCmd(ctx, command)
   292  	_, err := cmd.Output()
   293  	return err
   294  }
   295  
   296  func validateEquality(
   297  	options map[string]string,
   298  	cases map[string]marshal.StringList,
   299  	compare func(string, string) bool,
   300  ) error {
   301  	for optionName, values := range cases {
   302  		actual, ok := options[optionName]
   303  		if !ok {
   304  			continue
   305  		}
   306  
   307  		if err := validateOneOf(
   308  			fmt.Sprintf("option %q", optionName),
   309  			actual,
   310  			values,
   311  			compare,
   312  		); err == nil {
   313  			return nil
   314  		}
   315  	}
   316  
   317  	return newCondFailError("no options matched")
   318  }
   319  
   320  // WhenList is a list of when items with custom yaml unmarshaling.
   321  type WhenList []When
   322  
   323  // UnmarshalYAML allows single items to be used as lists.
   324  func (l *WhenList) UnmarshalYAML(unmarshal func(interface{}) error) error {
   325  	var whenSlice []When
   326  	sliceCandidate := marshal.UnmarshalCandidate{
   327  		Unmarshal: func() error { return unmarshal(&whenSlice) },
   328  		Assign:    func() { *l = whenSlice },
   329  	}
   330  
   331  	var whenItem When
   332  	itemCandidate := marshal.UnmarshalCandidate{
   333  		Unmarshal: func() error { return unmarshal(&whenItem) },
   334  		Assign:    func() { *l = WhenList{whenItem} },
   335  	}
   336  
   337  	return marshal.UnmarshalOneOf(sliceCandidate, itemCandidate)
   338  }
   339  
   340  // Validate returns an error if any when clauses fail.
   341  func (l *WhenList) Validate(ctx Context, vars map[string]string) error {
   342  	if l == nil {
   343  		return nil
   344  	}
   345  
   346  	for _, w := range *l {
   347  		if err := w.Validate(ctx, vars); err != nil {
   348  			return err
   349  		}
   350  	}
   351  
   352  	return nil
   353  }
   354  
   355  // Dependencies returns a list of options that are required explicitly.
   356  // This does not include interpolations.
   357  func (l *WhenList) Dependencies() []string {
   358  	if l == nil {
   359  		return nil
   360  	}
   361  
   362  	// Use a map to prevent duplicates
   363  	references := make(map[string]struct{})
   364  
   365  	for _, w := range *l {
   366  		for _, opt := range w.Dependencies() {
   367  			references[opt] = struct{}{}
   368  		}
   369  	}
   370  
   371  	options := make([]string, 0, len(references))
   372  	for opt := range references {
   373  		options = append(options, opt)
   374  	}
   375  
   376  	return options
   377  }