github.com/clusterize-io/tusk@v0.6.3-0.20211001020217-cfe8a8cd0d4a/runner/parse.go (about) 1 package runner 2 3 import ( 4 "fmt" 5 6 "github.com/clusterize-io/tusk/marshal" 7 yaml "gopkg.in/yaml.v2" 8 ) 9 10 // Parse loads the contents of a config file into a struct. 11 func Parse(text []byte) (*Config, error) { 12 cfg := new(Config) 13 14 if err := yaml.UnmarshalStrict(text, cfg); err != nil { 15 return nil, err 16 } 17 18 return cfg, nil 19 } 20 21 // ParseComplete parses the file completely with interpolation. 22 func ParseComplete( 23 meta *Metadata, 24 taskName string, 25 args []string, 26 flags map[string]string, 27 ) (*Config, error) { 28 cfg, err := Parse(meta.CfgText) 29 if err != nil { 30 return nil, err 31 } 32 33 t, isTaskSet := cfg.Tasks[taskName] 34 if !isTaskSet { 35 return cfg, nil 36 } 37 38 passed, err := combineArgsAndFlags(t, args, flags) 39 if err != nil { 40 return nil, err 41 } 42 43 ctx := Context{ 44 Interpreter: meta.Interpreter, 45 } 46 47 if err := passTaskValues(ctx, t, cfg, passed); err != nil { 48 return nil, err 49 } 50 51 return cfg, nil 52 } 53 54 func combineArgsAndFlags( 55 t *Task, args []string, flags map[string]string, 56 ) (map[string]string, error) { 57 if len(t.Args) != len(args) { 58 return nil, fmt.Errorf( 59 "task %q requires exactly %d args, got %d", 60 t.Name, len(t.Args), len(args), 61 ) 62 } 63 64 passed := make(map[string]string, len(args)+len(flags)) 65 for i, arg := range t.Args { 66 passed[arg.Name] = args[i] 67 } 68 for name, value := range flags { 69 passed[name] = value 70 } 71 72 return passed, nil 73 } 74 75 func passTaskValues( 76 ctx Context, 77 t *Task, 78 cfg *Config, 79 passed map[string]string, 80 ) error { 81 vars, err := interpolateGlobalOptions(ctx, t, cfg, passed) 82 if err != nil { 83 return err 84 } 85 86 if err := interpolateTask(ctx, t, passed, vars); err != nil { 87 return err 88 } 89 90 return addSubTasks(ctx, t, cfg) 91 } 92 93 func interpolateGlobalOptions( 94 ctx Context, 95 t *Task, 96 cfg *Config, 97 passed map[string]string, 98 ) (map[string]string, error) { 99 globalOptions, err := getRequiredGlobalOptions(t, cfg) 100 if err != nil { 101 return nil, err 102 } 103 104 vars := make(map[string]string, len(globalOptions)) 105 for _, o := range globalOptions { 106 if err := interpolateOption(ctx, o, passed, vars); err != nil { 107 return nil, err 108 } 109 } 110 111 return vars, nil 112 } 113 114 func getRequiredGlobalOptions(t *Task, cfg *Config) (Options, error) { 115 required, err := FindAllOptions(t, cfg) 116 if err != nil { 117 return nil, err 118 } 119 120 var output Options 121 for _, o := range cfg.Options { 122 for _, r := range required { 123 if r.Name != o.Name { 124 continue 125 } 126 127 output = append(output, o) 128 } 129 } 130 131 return output, nil 132 } 133 134 func interpolateArg(a *Arg, passed, vars map[string]string) error { 135 if err := marshal.Interpolate(a, vars); err != nil { 136 return err 137 } 138 139 valuePassed, ok := passed[a.Name] 140 if !ok { 141 return fmt.Errorf("no value passed for arg %q", a.Name) 142 } 143 144 a.Passed = valuePassed 145 146 value, err := a.Evaluate() 147 if err != nil { 148 return err 149 } 150 151 vars[a.Name] = value 152 153 return nil 154 } 155 156 func interpolateOption(ctx Context, o *Option, passed, vars map[string]string) error { 157 if err := marshal.Interpolate(o, vars); err != nil { 158 return err 159 } 160 161 if valuePassed, ok := passed[o.Name]; ok { 162 o.Passed = valuePassed 163 } 164 165 value, err := o.Evaluate(ctx, vars) 166 if err != nil { 167 return err 168 } 169 170 vars[o.Name] = value 171 172 return nil 173 } 174 175 func interpolateTask(ctx Context, t *Task, passed, vars map[string]string) error { 176 taskVars := make(map[string]string, len(vars)+len(t.Args)+len(t.Options)) 177 for k, v := range vars { 178 taskVars[k] = v 179 } 180 181 for _, a := range t.Args { 182 if err := interpolateArg(a, passed, taskVars); err != nil { 183 return err 184 } 185 } 186 187 for _, o := range t.Options { 188 if err := interpolateOption(ctx, o, passed, taskVars); err != nil { 189 return err 190 } 191 } 192 193 if err := marshal.Interpolate(&t.RunList, taskVars); err != nil { 194 return err 195 } 196 197 if err := marshal.Interpolate(&t.Finally, taskVars); err != nil { 198 return err 199 } 200 201 t.Vars = taskVars 202 203 return nil 204 } 205 206 func addSubTasks(ctx Context, t *Task, cfg *Config) error { 207 for _, run := range t.AllRunItems() { 208 for _, desc := range run.SubTaskList { 209 sub, err := newTaskFromSub(ctx, desc, cfg) 210 if err != nil { 211 return err 212 } 213 214 run.Tasks = append(run.Tasks, *sub) 215 } 216 } 217 218 return nil 219 } 220 221 func newTaskFromSub(ctx Context, desc *SubTask, cfg *Config) (*Task, error) { 222 st, ok := cfg.Tasks[desc.Name] 223 if !ok { 224 return nil, fmt.Errorf("sub-task %q does not exist", desc.Name) 225 } 226 227 subTask := copyTask(st) 228 229 values, err := getArgValues(subTask, desc.Args) 230 if err != nil { 231 return nil, err 232 } 233 234 for optName, opt := range desc.Options { 235 if _, isValidOption := subTask.Options.Lookup(optName); !isValidOption { 236 return nil, fmt.Errorf( 237 "option %q cannot be passed to task %q", 238 optName, subTask.Name, 239 ) 240 } 241 values[optName] = opt 242 } 243 244 if err := passTaskValues(ctx, subTask, cfg, values); err != nil { 245 return nil, err 246 } 247 248 return subTask, nil 249 } 250 251 // copyTask returns a copy of a task, replacing references with new values. 252 func copyTask(t *Task) *Task { 253 newTask := *t 254 255 argsCopy := make(Args, 0, len(newTask.Args)) 256 for _, ptr := range newTask.Args { 257 arg := *ptr 258 argsCopy = append(argsCopy, &arg) 259 } 260 newTask.Args = argsCopy 261 262 optionsCopy := make(Options, 0, len(newTask.Options)) 263 for _, ptr := range newTask.Options { 264 opt := *ptr 265 optionsCopy = append(optionsCopy, &opt) 266 } 267 newTask.Options = optionsCopy 268 269 return &newTask 270 } 271 272 func getArgValues(subTask *Task, argsPassed []string) (map[string]string, error) { 273 if len(argsPassed) != len(subTask.Args) { 274 return nil, fmt.Errorf( 275 "subtask %q requires %d args but got %d", 276 subTask.Name, len(subTask.Args), len(argsPassed), 277 ) 278 } 279 280 values := make(map[string]string) 281 for i, arg := range subTask.Args { 282 values[arg.Name] = argsPassed[i] 283 } 284 285 return values, nil 286 }