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

     1  package bob
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  
     7  	"github.com/Benchkram/bob/bob/bobfile"
     8  	"github.com/Benchkram/bob/pkg/ctl"
     9  	"github.com/Benchkram/errz"
    10  )
    11  
    12  // Examples of possible interactive usecase
    13  //
    14  // 1: [Done] executable requiring a database to run properly.
    15  //    Database is setup in a docker-compose file.
    16  //
    17  // 2: [Done] plain docker-compose run with dependcies to build-cmds
    18  //    containing instructions how to build the container image.
    19  //
    20  // TODO:
    21  // 3: init script requiring a executable to run before
    22  //    containing a health endpoint (REST?). So the init script can be
    23  //    sure about the service to be functional.
    24  //
    25  
    26  // Run builds dependent tasks for a run cmd and starts it.
    27  // A control is returned to interact with the run cmd.
    28  //
    29  // Canceling the cmd from the outside must be done through the context.
    30  //
    31  // TODO: Forbid circular dependecys.
    32  func (b *B) Run(ctx context.Context, runName string) (_ ctl.Commander, err error) {
    33  	defer errz.Recover(&err)
    34  
    35  	aggregate, err := b.Aggregate()
    36  	errz.Fatal(err)
    37  
    38  	b.PrintVersionCompatibility(aggregate)
    39  
    40  	runTask, ok := aggregate.RTasks[runName]
    41  	if !ok {
    42  		return nil, ErrRunDoesNotExist
    43  	}
    44  
    45  	// gather interactive tasks
    46  	childInteractiveTasks := b.interactiveTasksInChain(runName, aggregate)
    47  	interactiveTasks := []string{runTask.Name()}
    48  	interactiveTasks = append(interactiveTasks, childInteractiveTasks...)
    49  
    50  	// build dependencies & main runTask
    51  	for _, task := range interactiveTasks {
    52  		err = buildNonInteractive(ctx, task, aggregate)
    53  		errz.Fatal(err)
    54  	}
    55  
    56  	// generate run controls to steer the run cmd.
    57  	runCtls := []ctl.Command{}
    58  	for _, name := range interactiveTasks {
    59  		interactiveTask := aggregate.RTasks[name]
    60  
    61  		rc, err := interactiveTask.Run(ctx)
    62  		errz.Fatal(err)
    63  
    64  		runCtls = append(runCtls, rc)
    65  	}
    66  
    67  	builder := NewBuilder(b, runName, aggregate, buildNonInteractive)
    68  	commander := ctl.NewCommander(ctx, builder, runCtls...)
    69  
    70  	return commander, nil
    71  }
    72  
    73  // interactiveTasksInChain returns run tasks in the dependency chain.
    74  // Task on a higher level in the tree appear at the front of the slice..
    75  //
    76  // It will not error but return a empty error in case the runName
    77  // does not exists.
    78  func (b *B) interactiveTasksInChain(runName string, aggregate *bobfile.Bobfile) []string {
    79  	runTasks := []string{}
    80  
    81  	run, ok := aggregate.RTasks[runName]
    82  	if !ok {
    83  		return nil
    84  	}
    85  
    86  	for _, task := range run.DependsOn {
    87  		if !isInteractive(task, aggregate) {
    88  			continue
    89  		}
    90  		runTasks = append(runTasks, task)
    91  
    92  		// assure all it's dependent runTasks are also added.
    93  		childs := b.interactiveTasksInChain(task, aggregate)
    94  		runTasks = append(runTasks, childs...)
    95  	}
    96  
    97  	return normalize(runTasks)
    98  }
    99  
   100  // normalize removes duplicated entrys from the run task list.
   101  // The duplicate closest to the top of the chain is removed
   102  // so that child tasks are started first.
   103  func normalize(tasks []string) []string {
   104  	sanitized := []string{}
   105  
   106  	for i, task := range tasks {
   107  		keep := true
   108  
   109  		// last element can always be added safely
   110  		if i < len(tasks) {
   111  			for _, jtask := range tasks[i+1:] {
   112  				if task == jtask {
   113  					keep = false
   114  					break
   115  				}
   116  			}
   117  		}
   118  
   119  		if keep {
   120  			sanitized = append(sanitized, task)
   121  		}
   122  	}
   123  
   124  	return sanitized
   125  }
   126  
   127  func isInteractive(name string, aggregate *bobfile.Bobfile) bool {
   128  	_, ok := aggregate.RTasks[name]
   129  	return ok
   130  }
   131  
   132  // func isNonInteractive(name string, aggregate *bobfile.Bobfile) bool {
   133  // 	_, ok := aggregate.Tasks[name]
   134  // 	return ok
   135  // }
   136  
   137  // buildNonInteractive takes a interactive task to build it's non-interactive children.
   138  func buildNonInteractive(ctx context.Context, runname string, aggregate *bobfile.Bobfile) (err error) {
   139  	defer errz.Recover(&err)
   140  
   141  	interactive, ok := aggregate.RTasks[runname]
   142  	if !ok {
   143  		return ErrRunDoesNotExist
   144  	}
   145  
   146  	// Run dependent build tasks
   147  	// before starting the run task
   148  	for _, child := range interactive.DependsOn {
   149  		if isInteractive(child, aggregate) {
   150  			continue
   151  		}
   152  
   153  		playbook, err := aggregate.Playbook(child)
   154  		if err != nil {
   155  			if errors.Is(err, ErrTaskDoesNotExist) {
   156  				continue
   157  			}
   158  			errz.Fatal(err)
   159  		}
   160  
   161  		err = playbook.Build(ctx)
   162  		errz.Fatal(err)
   163  	}
   164  
   165  	return nil
   166  }