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  }