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 }