github.com/Benchkram/bob@v0.0.0-20220321080157-7c8f3876e225/bob/playbook/playbook.go (about)

     1  package playbook
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"sort"
     8  	"time"
     9  
    10  	"github.com/Benchkram/bob/bobtask"
    11  	"github.com/Benchkram/bob/bobtask/buildinfo"
    12  	"github.com/Benchkram/bob/bobtask/hash"
    13  	"github.com/Benchkram/bob/pkg/boblog"
    14  	"github.com/Benchkram/bob/pkg/buildinfostore"
    15  	"github.com/Benchkram/errz"
    16  	"github.com/logrusorgru/aurora"
    17  )
    18  
    19  // The playbook defines the order in which tasks are allowed to run.
    20  // Also determines the possibility to run tasks in parallel.
    21  
    22  var ErrTaskDoesNotExist = fmt.Errorf("task does not exist")
    23  var ErrDone = fmt.Errorf("playbook is done")
    24  var ErrFailed = fmt.Errorf("playbook failed")
    25  var ErrUnexpectedTaskState = fmt.Errorf("task state is unsexpected")
    26  
    27  type Playbook struct {
    28  	// taskChannel is closed when the root
    29  	// task completes.
    30  	taskChannel chan bobtask.Task
    31  
    32  	// errorChannel to transport errors to the caller
    33  	errorChannel chan error
    34  
    35  	// root task
    36  	root string
    37  
    38  	Tasks StatusMap
    39  
    40  	namePad int
    41  
    42  	done bool
    43  
    44  	// start is the point in time the playbook started
    45  	start time.Time
    46  	// end is the point in time the playbook ended
    47  	end time.Time
    48  
    49  	// enableCaching allows artifacts to be read & written to a store.
    50  	// Default: true.
    51  	enableCaching bool
    52  }
    53  
    54  func New(root string, opts ...Option) *Playbook {
    55  	p := &Playbook{
    56  		taskChannel:   make(chan bobtask.Task, 10),
    57  		errorChannel:  make(chan error),
    58  		Tasks:         make(StatusMap),
    59  		enableCaching: true,
    60  		root:          root,
    61  	}
    62  
    63  	for _, opt := range opts {
    64  		if opt == nil {
    65  			continue
    66  		}
    67  		opt(p)
    68  	}
    69  
    70  	return p
    71  }
    72  
    73  type RebuildCause string
    74  
    75  func (rc *RebuildCause) String() string {
    76  	return string(*rc)
    77  }
    78  
    79  const (
    80  	TaskInputChanged  RebuildCause = "input-changed"
    81  	TaskForcedRebuild RebuildCause = "forced"
    82  	DependencyChanged RebuildCause = "dependency-changed"
    83  	TargetInvalid     RebuildCause = "target-invalid"
    84  )
    85  
    86  // TaskNeedsRebuild check if a tasks need a rebuild by looking at it's hash value
    87  // and it's child tasks.
    88  func (p *Playbook) TaskNeedsRebuild(taskname string, hashIn hash.In) (rebuildRequired bool, cause RebuildCause, err error) {
    89  	ts, ok := p.Tasks[taskname]
    90  	if !ok {
    91  		return false, "", ErrTaskDoesNotExist
    92  	}
    93  	task := ts.Task
    94  	coloredName := task.ColoredName()
    95  
    96  	// returns true if rebuild strategy set to `always`
    97  	if task.Rebuild() == bobtask.RebuildAlways {
    98  		boblog.Log.V(3).Info(fmt.Sprintf("%-*s\tREBUILDING\t(rebuild set to always)", p.namePad, coloredName))
    99  		return true, TaskForcedRebuild, nil
   100  	}
   101  
   102  	rebuildRequired, err = task.NeedsRebuild(&bobtask.RebuildOptions{HashIn: &hashIn})
   103  	errz.Fatal(err)
   104  	if rebuildRequired {
   105  		boblog.Log.V(3).Info(fmt.Sprintf("%-*s\tNEEDS REBUILD\t(input changed)", p.namePad, coloredName))
   106  		return true, TaskInputChanged, nil
   107  	}
   108  
   109  	var Done = fmt.Errorf("done")
   110  	// Check if task needs a rebuild due to its dependencies changing
   111  	err = p.Tasks.walk(task.Name(), func(tn string, t *Status, err error) error {
   112  		if err != nil {
   113  			return err
   114  		}
   115  
   116  		// TODO: In case the task does not exist check if a artifact can be used?
   117  		//       Part of no-permission-workflow.
   118  
   119  		// Ignore the task itself
   120  		if task.Name() == tn {
   121  			return nil
   122  		}
   123  
   124  		// Require a rebuild if the dependend task did require a rebuild
   125  		if t.State() != StateNoRebuildRequired {
   126  			boblog.Log.V(3).Info(fmt.Sprintf("%-*s\tNEEDS REBUILD\t(dependecy changed)", p.namePad, coloredName))
   127  			rebuildRequired = true
   128  			// Bail out early
   129  			return Done
   130  		}
   131  
   132  		return nil
   133  	})
   134  
   135  	if errors.Is(err, Done) {
   136  		return true, DependencyChanged, nil
   137  	}
   138  
   139  	if !rebuildRequired {
   140  		// check rebuild due to invalidated targets
   141  		target, err := task.Target()
   142  		if err != nil {
   143  			return true, "", err
   144  		}
   145  		if target != nil {
   146  			// On a invalid traget a rebuild is required
   147  			rebuildRequired = !target.Verify()
   148  
   149  			// Try to load a target from the store when a rebuild is required.
   150  			// If not assure the artifact exists in the store.
   151  			if rebuildRequired {
   152  				boblog.Log.V(2).Info(fmt.Sprintf("[task:%s] trying to get target from store", taskname))
   153  				ok, err := task.ArtifactUnpack(hashIn)
   154  				boblog.Log.Error(err, "Unable to get target from store")
   155  
   156  				if ok {
   157  					rebuildRequired = false
   158  				} else {
   159  					boblog.Log.V(3).Info(fmt.Sprintf("[task:%s] failed to get target from store", taskname))
   160  				}
   161  			} else {
   162  				if !task.ArtifactExists(hashIn) && p.enableCaching {
   163  					err = task.ArtifactPack(hashIn)
   164  					boblog.Log.Error(err, "Unable to send target to store")
   165  				}
   166  			}
   167  
   168  			if rebuildRequired {
   169  				boblog.Log.V(3).Info(fmt.Sprintf("%-*s\tNEEDS REBUILD\t(invalid targets)", p.namePad, coloredName))
   170  			}
   171  		}
   172  	}
   173  
   174  	return rebuildRequired, TargetInvalid, err
   175  }
   176  
   177  func (p *Playbook) Play() (err error) {
   178  	return p.play()
   179  }
   180  
   181  func (p *Playbook) play() error {
   182  
   183  	if p.done {
   184  		return ErrDone
   185  	}
   186  
   187  	if p.start.IsZero() {
   188  		p.start = time.Now()
   189  	}
   190  
   191  	// Walk the task chain and determine the next build task. Send it to the task channel.
   192  	// Returns `taskQueued` when a task has been send to the taskChannel.
   193  	// Returns `taskFailed` when a task has failed.
   194  	// Once it returns `nil` the playbook is done with it's work.
   195  	var taskQueued = fmt.Errorf("task queued")
   196  	var taskFailed = fmt.Errorf("task failed")
   197  	err := p.Tasks.walk(p.root, func(taskname string, task *Status, err error) error {
   198  		if err != nil {
   199  			return err
   200  		}
   201  
   202  		// fmt.Printf("walking task %s which is in state %s\n", taskname, task.State())
   203  
   204  		switch task.State() {
   205  		case StatePending:
   206  			// Check if all dependent tasks are completed
   207  			for _, dependentTaskName := range task.Task.DependsOn {
   208  				t, ok := p.Tasks[dependentTaskName]
   209  				if !ok {
   210  					//fmt.Printf("Task %s does not exist", dependentTaskName)
   211  					return ErrTaskDoesNotExist
   212  				}
   213  				// fmt.Printf("dependentTask %s which is in state %s\n", t.Task.Name(), t.State())
   214  
   215  				state := t.State()
   216  				if state != StateCompleted && state != StateNoRebuildRequired {
   217  					// A dependent task is not completed.
   218  					// So this task is not yet ready to run.
   219  					return nil
   220  				}
   221  			}
   222  		case StateFailed:
   223  			return taskFailed
   224  		case StateCanceled:
   225  			return nil
   226  		case StateNoRebuildRequired:
   227  			return nil
   228  		case StateCompleted:
   229  			return nil
   230  		default:
   231  		}
   232  
   233  		// fmt.Printf("sending task %s to channel\n", task.Task.Name())
   234  		// setting the task start time before passing it to channel
   235  		task.Start = time.Now()
   236  		p.taskChannel <- task.Task
   237  		return taskQueued
   238  	})
   239  
   240  	// taskQueued => return nil (happy path)
   241  	// taskFailed => return PlaybookFailed
   242  	// default    => return err
   243  	if err != nil {
   244  		if errors.Is(err, taskQueued) {
   245  			return nil
   246  		}
   247  		if errors.Is(err, taskFailed) {
   248  			return ErrFailed
   249  		}
   250  		return err
   251  	}
   252  
   253  	// no work done, usually happens when
   254  	// no task needs a rebuild.
   255  	p.Done()
   256  
   257  	return nil
   258  }
   259  
   260  func (p *Playbook) Done() {
   261  	if !p.done {
   262  		p.done = true
   263  		p.end = time.Now()
   264  		close(p.taskChannel)
   265  	}
   266  }
   267  
   268  // TaskChannel returns the next task
   269  func (p *Playbook) TaskChannel() <-chan bobtask.Task {
   270  	return p.taskChannel
   271  }
   272  
   273  func (p *Playbook) ErrorChannel() <-chan error {
   274  	return p.errorChannel
   275  }
   276  
   277  func (p *Playbook) setTaskState(taskname string, state State, taskError error) error {
   278  	task, ok := p.Tasks[taskname]
   279  	if !ok {
   280  		return ErrTaskDoesNotExist
   281  	}
   282  
   283  	task.SetState(state, taskError)
   284  	switch state {
   285  	case StateCompleted, StateCanceled, StateNoRebuildRequired:
   286  		task.End = time.Now()
   287  	}
   288  
   289  	p.Tasks[taskname] = task
   290  	return nil
   291  }
   292  
   293  func (p *Playbook) pack(taskname string, hash hash.In) error {
   294  	task, ok := p.Tasks[taskname]
   295  	if !ok {
   296  		return ErrTaskDoesNotExist
   297  	}
   298  	return task.Task.ArtifactPack(hash)
   299  }
   300  
   301  func (p *Playbook) storeHash(taskname string, buildinfo *buildinfo.I) error {
   302  	task, ok := p.Tasks[taskname]
   303  	if !ok {
   304  		return ErrTaskDoesNotExist
   305  	}
   306  
   307  	return task.Task.WriteBuildinfo(buildinfo)
   308  }
   309  
   310  func (p *Playbook) ExecutionTime() time.Duration {
   311  	return p.end.Sub(p.start)
   312  }
   313  
   314  // TaskStatus returns the current state of a task
   315  func (p *Playbook) TaskStatus(taskname string) (ts *Status, _ error) {
   316  	status, ok := p.Tasks[taskname]
   317  	if !ok {
   318  		return ts, ErrTaskDoesNotExist
   319  	}
   320  	return status, nil
   321  }
   322  
   323  // TaskCompleted sets a task to completed
   324  func (p *Playbook) TaskCompleted(taskname string) (err error) {
   325  	defer errz.Recover(&err)
   326  
   327  	task, ok := p.Tasks[taskname]
   328  	if !ok {
   329  		return ErrTaskDoesNotExist
   330  	}
   331  
   332  	// compute input hash
   333  	hashIn, err := task.Task.HashIn()
   334  	errz.Fatal(err)
   335  
   336  	buildInfo, err := task.ReadBuildinfo()
   337  	if err != nil {
   338  		if errors.Is(err, buildinfostore.ErrBuildInfoDoesNotExist) {
   339  			// assure buildinfo is initialized correctly
   340  			buildInfo = buildinfo.New()
   341  		} else {
   342  			errz.Fatal(err)
   343  		}
   344  	}
   345  	buildInfo.Info.Taskname = task.Name()
   346  
   347  	target, err := task.Task.Target()
   348  	errz.Fatal(err)
   349  
   350  	if target != nil {
   351  		targetHash, err := target.Hash()
   352  		if err != nil {
   353  			return err
   354  		}
   355  
   356  		buildInfo.Targets[hashIn] = targetHash
   357  
   358  		// gather target hashes of dependent tasks
   359  		err = p.Tasks.walk(taskname, func(tn string, task *Status, err error) error {
   360  			if err != nil {
   361  				return err
   362  			}
   363  			if taskname == tn {
   364  				return nil
   365  			}
   366  
   367  			target, err := task.Target()
   368  			if err != nil {
   369  				return err
   370  			}
   371  			if target == nil {
   372  				return nil
   373  			}
   374  
   375  			switch task.State() {
   376  			case StateCompleted:
   377  				fallthrough
   378  			case StateNoRebuildRequired:
   379  				h, err := target.Hash()
   380  				if err != nil {
   381  					return err
   382  				}
   383  				hashIn, err := task.HashIn()
   384  				if err != nil {
   385  					return err
   386  				}
   387  				buildInfo.Targets[hashIn] = h
   388  			default:
   389  				boblog.Log.V(1).Info(string(task.state))
   390  				return ErrUnexpectedTaskState
   391  			}
   392  
   393  			return nil
   394  		})
   395  		errz.Fatal(err)
   396  	}
   397  
   398  	err = p.storeHash(taskname, buildInfo)
   399  	errz.Fatal(err)
   400  
   401  	// TODO: use target hash?
   402  	if p.enableCaching {
   403  		err = p.pack(taskname, hashIn)
   404  		errz.Fatal(err)
   405  	}
   406  
   407  	err = p.setTaskState(taskname, StateCompleted, nil)
   408  	errz.Fatal(err)
   409  
   410  	err = p.play()
   411  	if err != nil {
   412  		if !errors.Is(err, ErrDone) {
   413  			errz.Fatal(err)
   414  		}
   415  	}
   416  
   417  	return nil
   418  }
   419  
   420  // TaskNoRebuildRequired sets a task's state to indicate that no rebuild is required
   421  func (p *Playbook) TaskNoRebuildRequired(taskname string) (err error) {
   422  	defer errz.Recover(&err)
   423  
   424  	err = p.setTaskState(taskname, StateNoRebuildRequired, nil)
   425  	errz.Fatal(err)
   426  
   427  	err = p.play()
   428  	if err != nil {
   429  		if !errors.Is(err, ErrDone) {
   430  			errz.Fatal(err)
   431  		}
   432  	}
   433  
   434  	return nil
   435  }
   436  
   437  // TaskFailed sets a task to failed
   438  func (p *Playbook) TaskFailed(taskname string, taskErr error) (err error) {
   439  	defer errz.Recover(&err)
   440  
   441  	err = p.setTaskState(taskname, StateFailed, taskErr)
   442  	errz.Fatal(err)
   443  
   444  	// p.errorChannel <- fmt.Errorf("Task %s failed", taskname)
   445  
   446  	// give the playbook the chance to set
   447  	// the state to done.
   448  	_ = p.play()
   449  
   450  	return nil
   451  }
   452  
   453  // TaskCanceled sets a task to canceled
   454  func (p *Playbook) TaskCanceled(taskname string) (err error) {
   455  	defer errz.Recover(&err)
   456  
   457  	err = p.setTaskState(taskname, StateCanceled, nil)
   458  	errz.Fatal(err)
   459  
   460  	// p.errorChannel <- fmt.Errorf("Task %s cancelled", taskname)
   461  
   462  	return nil
   463  }
   464  
   465  func (p *Playbook) List() (err error) {
   466  	defer errz.Recover(&err)
   467  
   468  	keys := make([]string, 0, len(p.Tasks))
   469  	for k := range p.Tasks {
   470  		keys = append(keys, k)
   471  	}
   472  	sort.Strings(keys)
   473  
   474  	for _, k := range keys {
   475  		fmt.Println(k)
   476  	}
   477  
   478  	return nil
   479  }
   480  
   481  func (p *Playbook) String() string {
   482  	description := bytes.NewBufferString("")
   483  
   484  	fmt.Fprint(description, "Playbook:\n")
   485  
   486  	keys := make([]string, 0, len(p.Tasks))
   487  	for k := range p.Tasks {
   488  		keys = append(keys, k)
   489  	}
   490  	sort.Strings(keys)
   491  
   492  	for _, k := range keys {
   493  		task := p.Tasks[k]
   494  		fmt.Fprintf(description, "  %s(%s): %s\n", k, task.Task.Name(), task.State())
   495  	}
   496  
   497  	return description.String()
   498  }
   499  
   500  type State string
   501  
   502  // Summary state indicators.
   503  // The nbsp are intended to align on the cli.
   504  func (s *State) Summary() string {
   505  	switch *s {
   506  	case StatePending:
   507  		return "⌛       "
   508  	case StateCompleted:
   509  		return aurora.Green("✔").Bold().String() + "       "
   510  	case StateNoRebuildRequired:
   511  		return aurora.Green("cached").String() + "  "
   512  	case StateFailed:
   513  		return aurora.Red("failed").String() + "  "
   514  	case StateCanceled:
   515  		return aurora.Faint("canceled").String()
   516  	default:
   517  		return ""
   518  	}
   519  }
   520  
   521  func (s *State) Short() string {
   522  	switch *s {
   523  	case StatePending:
   524  		return "pending"
   525  	case StateCompleted:
   526  		return "done"
   527  	case StateNoRebuildRequired:
   528  		return "cached"
   529  	case StateFailed:
   530  		return "failed"
   531  	case StateCanceled:
   532  		return "canceled"
   533  	default:
   534  		return ""
   535  	}
   536  }
   537  
   538  const (
   539  	StatePending           State = "PENDING"
   540  	StateCompleted         State = "COMPLETED"
   541  	StateNoRebuildRequired State = "CACHED"
   542  	StateFailed            State = "FAILED"
   543  	StateCanceled          State = "CANCELED"
   544  )