github.com/clusterize-io/tusk@v0.6.3-0.20211001020217-cfe8a8cd0d4a/runner/task.go (about) 1 package runner 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 8 "github.com/clusterize-io/tusk/marshal" 9 yaml "gopkg.in/yaml.v2" 10 ) 11 12 // executionState indicates whether a task is "running" or "finally". 13 type executionState int 14 15 const ( 16 stateRunning executionState = iota 17 stateFinally executionState = iota 18 ) 19 20 // Task is a single task to be run by CLI. 21 type Task struct { 22 Args Args `yaml:"args,omitempty"` 23 Options Options `yaml:"options,omitempty"` 24 25 RunList RunList `yaml:"run"` 26 Finally RunList `yaml:"finally,omitempty"` 27 Usage string `yaml:",omitempty"` 28 Description string `yaml:",omitempty"` 29 Private bool 30 31 // Computed members not specified in yaml file 32 Name string `yaml:"-"` 33 Vars map[string]string `yaml:"-"` 34 } 35 36 // UnmarshalYAML unmarshals and assigns names to options. 37 func (t *Task) UnmarshalYAML(unmarshal func(interface{}) error) error { 38 var includeTarget Task 39 includeCandidate := marshal.UnmarshalCandidate{ 40 Unmarshal: func() error { 41 var def struct { 42 Include string `yaml:"include"` 43 Else map[string]string `yaml:",inline"` 44 } 45 46 if err := unmarshal(&def); err != nil { 47 return err 48 } 49 50 if def.Include == "" { 51 // A yaml.TypeError signals to keep trying other candidates. 52 return &yaml.TypeError{Errors: []string{`"include" not specified`}} 53 } 54 55 if len(def.Else) != 0 { 56 return errors.New(`tasks using "include" may not specify other fields`) 57 } 58 59 f, err := os.Open(def.Include) 60 if err != nil { 61 return fmt.Errorf("opening included file: %w", err) 62 } 63 defer f.Close() // nolint: errcheck 64 65 decoder := yaml.NewDecoder(f) 66 decoder.SetStrict(true) 67 68 if err := decoder.Decode(&includeTarget); err != nil { 69 return fmt.Errorf("decoding included file %q: %w", def.Include, err) 70 } 71 72 return nil 73 }, 74 Assign: func() { *t = includeTarget }, 75 } 76 77 var taskTarget Task 78 taskCandidate := marshal.UnmarshalCandidate{ 79 Unmarshal: func() error { 80 type taskType Task // Use new type to avoid recursion 81 return unmarshal((*taskType)(&taskTarget)) 82 }, 83 Validate: taskTarget.checkOptArgCollisions, 84 Assign: func() { *t = taskTarget }, 85 } 86 87 return marshal.UnmarshalOneOf(includeCandidate, taskCandidate) 88 } 89 90 // includedTask is the configuration for reading a task definition from another 91 // file. 92 func (t *Task) checkOptArgCollisions() error { 93 for _, o := range t.Options { 94 for _, a := range t.Args { 95 if o.Name == a.Name { 96 return fmt.Errorf( 97 "argument and option %q must have unique names within a task", o.Name, 98 ) 99 } 100 } 101 } 102 103 return nil 104 } 105 106 // AllRunItems returns all run items referenced, including `run` and `finally`. 107 func (t *Task) AllRunItems() RunList { 108 return append(t.RunList, t.Finally...) 109 } 110 111 // Dependencies returns a list of options that are required explicitly. 112 // This does not include interpolations. 113 func (t *Task) Dependencies() []string { 114 options := make([]string, 0, len(t.Options)+len(t.AllRunItems())) 115 116 for _, opt := range t.Options { 117 options = append(options, opt.Dependencies()...) 118 } 119 for _, run := range t.AllRunItems() { 120 options = append(options, run.When.Dependencies()...) 121 } 122 123 return options 124 } 125 126 // Execute runs the Run scripts in the task. 127 func (t *Task) Execute(ctx Context) (err error) { 128 if !t.Private { 129 ctx.PushTask(t) 130 } 131 132 ctx.Logger.PrintTask(t.Name) 133 134 defer ctx.Logger.PrintTaskCompleted(t.Name) 135 defer t.runFinally(ctx, &err) 136 137 for _, r := range t.RunList { 138 if rerr := t.run(ctx, r, stateRunning); rerr != nil { 139 return rerr 140 } 141 } 142 143 return err 144 } 145 146 func (t *Task) runFinally(ctx Context, err *error) { 147 if len(t.Finally) == 0 { 148 return 149 } 150 151 ctx.Logger.PrintTaskFinally(t.Name) 152 153 for _, r := range t.Finally { 154 if rerr := t.run(ctx, r, stateFinally); rerr != nil { 155 // Do not overwrite existing errors 156 if *err == nil { 157 *err = rerr 158 } 159 return 160 } 161 } 162 } 163 164 // run executes a Run struct. 165 func (t *Task) run(ctx Context, r *Run, s executionState) error { 166 if ok, err := r.shouldRun(ctx, t.Vars); !ok || err != nil { 167 return err 168 } 169 170 runFuncs := []func() error{ 171 func() error { return t.runCommands(ctx, r, s) }, 172 func() error { return t.runSubTasks(ctx, r) }, 173 func() error { return t.runEnvironment(ctx, r) }, 174 } 175 176 for _, f := range runFuncs { 177 if err := f(); err != nil { 178 return err 179 } 180 } 181 182 return nil 183 } 184 185 func (t *Task) runCommands(ctx Context, r *Run, s executionState) error { 186 for _, command := range r.Command { 187 switch s { 188 case stateFinally: 189 ctx.Logger.PrintCommandWithParenthetical(command.Print, "finally", ctx.Tasks()...) 190 default: 191 ctx.Logger.PrintCommand(command.Print, ctx.Tasks()...) 192 } 193 194 if err := command.exec(ctx); err != nil { 195 ctx.Logger.PrintCommandError(err) 196 return err 197 } 198 } 199 200 return nil 201 } 202 203 func (t *Task) runSubTasks(ctx Context, r *Run) error { 204 for i := range r.Tasks { 205 if err := r.Tasks[i].Execute(ctx); err != nil { 206 return err 207 } 208 } 209 210 return nil 211 } 212 213 func (t *Task) runEnvironment(ctx Context, r *Run) error { 214 ctx.Logger.PrintEnvironment(r.SetEnvironment) 215 for key, value := range r.SetEnvironment { 216 if value == nil { 217 if err := os.Unsetenv(key); err != nil { 218 return err 219 } 220 221 continue 222 } 223 224 if err := os.Setenv(key, *value); err != nil { 225 return err 226 } 227 } 228 229 return nil 230 }