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 }