github.com/dnephin/dobi@v0.15.0/tasks/tasks.go (about)

     1  package tasks
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  	"time"
     7  
     8  	"github.com/dnephin/dobi/config"
     9  	"github.com/dnephin/dobi/execenv"
    10  	"github.com/dnephin/dobi/logging"
    11  	"github.com/dnephin/dobi/tasks/alias"
    12  	"github.com/dnephin/dobi/tasks/client"
    13  	"github.com/dnephin/dobi/tasks/compose"
    14  	"github.com/dnephin/dobi/tasks/context"
    15  	"github.com/dnephin/dobi/tasks/env"
    16  	"github.com/dnephin/dobi/tasks/image"
    17  	"github.com/dnephin/dobi/tasks/job"
    18  	"github.com/dnephin/dobi/tasks/mount"
    19  	"github.com/dnephin/dobi/tasks/task"
    20  	"github.com/dnephin/dobi/tasks/types"
    21  	log "github.com/sirupsen/logrus"
    22  )
    23  
    24  // TaskCollection is a collection of Task objects
    25  type TaskCollection struct {
    26  	tasks []types.TaskConfig
    27  }
    28  
    29  func (c *TaskCollection) add(task types.TaskConfig) {
    30  	c.tasks = append(c.tasks, task)
    31  }
    32  
    33  // All returns all the tasks in the dependency order
    34  func (c *TaskCollection) All() []types.TaskConfig {
    35  	return c.tasks
    36  }
    37  
    38  // Get returns the TaskConfig for the Name
    39  func (c *TaskCollection) Get(name task.Name) types.TaskConfig {
    40  	for _, task := range c.tasks {
    41  		if task.Name().Equal(name) {
    42  			return task
    43  		}
    44  	}
    45  	return nil
    46  }
    47  
    48  func newTaskCollection() *TaskCollection {
    49  	return &TaskCollection{}
    50  }
    51  
    52  func collectTasks(options RunOptions) (*TaskCollection, error) {
    53  	return collect(options, &collectionState{
    54  		newTaskCollection(),
    55  		task.NewStack(),
    56  	})
    57  }
    58  
    59  type collectionState struct {
    60  	tasks     *TaskCollection
    61  	taskStack *task.Stack
    62  }
    63  
    64  func collect(options RunOptions, state *collectionState) (*TaskCollection, error) {
    65  	for _, taskname := range options.Tasks {
    66  		taskname := task.ParseName(taskname)
    67  		resourceName := taskname.Resource()
    68  		resource, ok := options.Config.Resources[resourceName]
    69  		if !ok {
    70  			return nil, fmt.Errorf("resource %q does not exist", resourceName)
    71  		}
    72  
    73  		taskConfig, err := buildTaskConfig(resourceName, taskname.Action(), resource)
    74  		if err != nil {
    75  			return nil, err
    76  		}
    77  
    78  		if state.taskStack.Contains(taskConfig.Name()) {
    79  			return nil, fmt.Errorf(
    80  				"Invalid dependency cycle: %s", strings.Join(state.taskStack.Names(), ", "))
    81  		}
    82  		state.taskStack.Push(taskConfig.Name())
    83  
    84  		options.Tasks = taskConfig.Dependencies()
    85  		if _, err := collect(options, state); err != nil {
    86  			return nil, err
    87  		}
    88  		state.tasks.add(taskConfig)
    89  		state.taskStack.Pop() // nolint: errcheck
    90  	}
    91  	return state.tasks, nil
    92  }
    93  
    94  // TODO: some way to make this a registry
    95  func buildTaskConfig(name, action string, resource config.Resource) (types.TaskConfig, error) {
    96  	switch conf := resource.(type) {
    97  	case *config.ImageConfig:
    98  		return image.GetTaskConfig(name, action, conf)
    99  	case *config.JobConfig:
   100  		return job.GetTaskConfig(name, action, conf)
   101  	case *config.MountConfig:
   102  		return mount.GetTaskConfig(name, action, conf)
   103  	case *config.AliasConfig:
   104  		return alias.GetTaskConfig(name, action, conf)
   105  	case *config.EnvConfig:
   106  		return env.GetTaskConfig(name, action, conf)
   107  	case *config.ComposeConfig:
   108  		return compose.GetTaskConfig(name, action, conf)
   109  	default:
   110  		panic(fmt.Sprintf("Unexpected config type %T", conf))
   111  	}
   112  }
   113  
   114  func reversed(tasks []types.Task) []types.Task {
   115  	reversed := []types.Task{}
   116  	for i := len(tasks) - 1; i >= 0; i-- {
   117  		reversed = append(reversed, tasks[i])
   118  	}
   119  	return reversed
   120  }
   121  
   122  func executeTasks(ctx *context.ExecuteContext, tasks *TaskCollection) error {
   123  	startedTasks := []types.Task{}
   124  
   125  	defer func() {
   126  		logging.Log.Debug("stopping tasks")
   127  		for _, startedTask := range reversed(startedTasks) {
   128  			if err := startedTask.Stop(ctx); err != nil {
   129  				logging.Log.Warnf("Failed to stop task %q: %s", startedTask.Name(), err)
   130  			}
   131  		}
   132  	}()
   133  
   134  	logging.Log.Debug("executing tasks")
   135  	for _, taskConfig := range tasks.All() {
   136  		resource, err := taskConfig.Resource().Resolve(ctx.Env)
   137  		if err != nil {
   138  			return err
   139  		}
   140  		ctx.Resources.Add(taskConfig.Name().Resource(), resource)
   141  
   142  		currentTask := taskConfig.Task(resource)
   143  		startedTasks = append(startedTasks, currentTask)
   144  		start := time.Now()
   145  		logging.Log.WithFields(log.Fields{"time": start, "task": currentTask}).Debug("Start")
   146  
   147  		depsModified := hasModifiedDeps(ctx, taskConfig.Dependencies())
   148  		modified, err := currentTask.Run(ctx, depsModified)
   149  		if err != nil {
   150  			return fmt.Errorf("failed to execute task %q: %s", currentTask.Name(), err)
   151  		}
   152  		if modified {
   153  			ctx.SetModified(currentTask.Name())
   154  		}
   155  		logging.Log.WithFields(log.Fields{
   156  			"elapsed": time.Since(start),
   157  			"task":    currentTask,
   158  		}).Debug("Complete")
   159  	}
   160  	return nil
   161  }
   162  
   163  func hasModifiedDeps(ctx *context.ExecuteContext, deps []string) bool {
   164  	for _, dep := range deps {
   165  		taskName := task.ParseName(dep)
   166  		if ctx.IsModified(taskName) {
   167  			return true
   168  		}
   169  	}
   170  	return false
   171  }
   172  
   173  // RunOptions are the options supported by Run
   174  type RunOptions struct {
   175  	Client    client.DockerClient
   176  	Config    *config.Config
   177  	Tasks     []string
   178  	Quiet     bool
   179  	BindMount bool
   180  }
   181  
   182  func getNames(options RunOptions) []string {
   183  	if len(options.Tasks) > 0 {
   184  		return options.Tasks
   185  	}
   186  
   187  	if options.Config.Meta.Default != "" {
   188  		return []string{options.Config.Meta.Default}
   189  	}
   190  
   191  	return options.Tasks
   192  }
   193  
   194  // Run one or more tasks
   195  func Run(options RunOptions) error {
   196  	options.Tasks = getNames(options)
   197  	if len(options.Tasks) == 0 {
   198  		return fmt.Errorf("no task to run, and no default task defined")
   199  	}
   200  
   201  	execEnv, err := execenv.NewExecEnvFromConfig(
   202  		options.Config.Meta.ExecID,
   203  		options.Config.Meta.Project,
   204  		options.Config.WorkingDir,
   205  	)
   206  	if err != nil {
   207  		return err
   208  	}
   209  
   210  	tasks, err := collectTasks(options)
   211  	if err != nil {
   212  		return err
   213  	}
   214  
   215  	ctx := context.NewExecuteContext(
   216  		options.Config,
   217  		options.Client,
   218  		execEnv,
   219  		context.NewSettings(options.Quiet, options.BindMount))
   220  	return executeTasks(ctx, tasks)
   221  }