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  }