github.com/leg100/ots@v0.0.7-0.20210919080622-034055ced4bd/executor.go (about)

     1  package ots
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"os/exec"
     9  
    10  	"github.com/go-logr/logr"
    11  )
    12  
    13  // Executor executes a job, providing it with services, temp directory etc,
    14  // capturing its stdout
    15  type Executor struct {
    16  	JobService
    17  
    18  	RunService                  RunService
    19  	ConfigurationVersionService ConfigurationVersionService
    20  	StateVersionService         StateVersionService
    21  
    22  	// Current working directory
    23  	Path string
    24  
    25  	// Whether cancelation has been triggered
    26  	canceled bool
    27  
    28  	// Cancel context func for currently running func
    29  	cancel context.CancelFunc
    30  
    31  	// Current process
    32  	proc *os.Process
    33  
    34  	// CLI process output is written to this
    35  	out io.WriteCloser
    36  
    37  	// logger
    38  	logr.Logger
    39  
    40  	// agentID is the ID of the agent hosting the execution executor
    41  	agentID string
    42  }
    43  
    44  // ExecutorFunc is a func that can be invoked in the executor
    45  type ExecutorFunc func(context.Context, *Executor) error
    46  
    47  // NewExecutor constructs an Executor.
    48  func NewExecutor(logger logr.Logger, rs RunService, cvs ConfigurationVersionService, svs StateVersionService, agentID string) (*Executor, error) {
    49  	path, err := os.MkdirTemp("", "ots-plan")
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  
    54  	return &Executor{
    55  		RunService:                  rs,
    56  		JobService:                  rs,
    57  		ConfigurationVersionService: cvs,
    58  		StateVersionService:         svs,
    59  		Path:                        path,
    60  		agentID:                     agentID,
    61  		Logger:                      logger,
    62  	}, nil
    63  }
    64  
    65  // Execute performs the full lifecycle of executing a job: marking it as
    66  // started, running the job, and then marking it as finished. Its logs are
    67  // captured and forwarded.
    68  func (e *Executor) Execute(job Job) (err error) {
    69  	job, err = e.Start(job.GetID(), JobStartOptions{AgentID: e.agentID})
    70  	if err != nil {
    71  		return fmt.Errorf("unable to start job: %w", err)
    72  	}
    73  
    74  	e.out = &JobWriter{
    75  		ID:              job.GetID(),
    76  		JobLogsUploader: e.JobService,
    77  		Logger:          e.Logger,
    78  	}
    79  
    80  	// Record whether job errored
    81  	var errored bool
    82  
    83  	e.Info("executing job", "status", job.GetStatus())
    84  
    85  	if err := job.Do(e); err != nil {
    86  		errored = true
    87  		e.Error(err, "unable to execute job")
    88  	}
    89  
    90  	// Mark the logs as fully uploaded
    91  	if err := e.out.Close(); err != nil {
    92  		errored = true
    93  		e.Error(err, "unable to finish writing logs")
    94  	}
    95  
    96  	// Regardless of job success, mark job as finished
    97  	_, err = e.Finish(job.GetID(), JobFinishOptions{Errored: errored})
    98  	if err != nil {
    99  		e.Error(err, "finishing job")
   100  		return err
   101  	}
   102  
   103  	e.Info("finished job")
   104  
   105  	return nil
   106  }
   107  
   108  // Cancel terminates execution. Force controls whether termination is graceful
   109  // or not. Performed on a best-effort basis - the func or process may have
   110  // finished before they are cancelled, in which case only the next func or
   111  // process will be stopped from executing.
   112  func (e *Executor) Cancel(force bool) {
   113  	e.canceled = true
   114  
   115  	e.cancelCLI(force)
   116  	e.cancelFunc(force)
   117  }
   118  
   119  // RunCLI executes a CLI process in the executor.
   120  func (e *Executor) RunCLI(name string, args ...string) error {
   121  	if e.canceled {
   122  		return fmt.Errorf("execution canceled")
   123  	}
   124  
   125  	cmd := exec.Command(name, args...)
   126  	cmd.Dir = e.Path
   127  	cmd.Stdout = e.out
   128  	cmd.Stderr = e.out
   129  
   130  	e.proc = cmd.Process
   131  
   132  	return cmd.Run()
   133  }
   134  
   135  // RunFunc invokes a func in the executor.
   136  func (e *Executor) RunFunc(fn ExecutorFunc) error {
   137  	if e.canceled {
   138  		return fmt.Errorf("execution canceled")
   139  	}
   140  
   141  	// Create and store cancel func so func's context can be canceled
   142  	ctx, cancel := context.WithCancel(context.Background())
   143  	e.cancel = cancel
   144  
   145  	return fn(ctx, e)
   146  }
   147  
   148  func (e *Executor) cancelCLI(force bool) {
   149  	if e.proc == nil {
   150  		return
   151  	}
   152  
   153  	if force {
   154  		e.proc.Signal(os.Kill)
   155  	} else {
   156  		e.proc.Signal(os.Interrupt)
   157  	}
   158  }
   159  
   160  func (e *Executor) cancelFunc(force bool) {
   161  	// Don't cancel a func's context unless forced
   162  	if !force {
   163  		return
   164  	}
   165  	if e.cancel == nil {
   166  		return
   167  	}
   168  	e.cancel()
   169  }