github.com/drone/runner-go@v1.12.0/pipeline/runtime/execer.go (about) 1 // Copyright 2019 Drone.IO Inc. All rights reserved. 2 // Use of this source code is governed by the Polyform License 3 // that can be found in the LICENSE file. 4 5 package runtime 6 7 import ( 8 "context" 9 "sync" 10 11 "github.com/drone/drone-go/drone" 12 "github.com/drone/runner-go/environ" 13 "github.com/drone/runner-go/livelog/extractor" 14 "github.com/drone/runner-go/logger" 15 "github.com/drone/runner-go/pipeline" 16 17 "github.com/hashicorp/go-multierror" 18 "github.com/natessilva/dag" 19 "golang.org/x/sync/semaphore" 20 ) 21 22 // Execer executes the pipeline. 23 type Execer struct { 24 mu sync.Mutex 25 engine Engine 26 reporter pipeline.Reporter 27 streamer pipeline.Streamer 28 uploader pipeline.Uploader 29 sem *semaphore.Weighted 30 } 31 32 // NewExecer returns a new execer. 33 func NewExecer( 34 reporter pipeline.Reporter, 35 streamer pipeline.Streamer, 36 uploader pipeline.Uploader, 37 engine Engine, 38 threads int64, 39 ) *Execer { 40 exec := &Execer{ 41 reporter: reporter, 42 streamer: streamer, 43 engine: engine, 44 uploader: uploader, 45 } 46 if threads > 0 { 47 // optional semaphore that limits the number of steps 48 // that can execute concurrently. 49 exec.sem = semaphore.NewWeighted(threads) 50 } 51 return exec 52 } 53 54 // Exec executes the intermediate representation of the pipeline 55 // and returns an error if execution fails. 56 func (e *Execer) Exec(ctx context.Context, spec Spec, state *pipeline.State) error { 57 log := logger.FromContext(ctx) 58 59 defer func() { 60 log.Debugln("destroying the pipeline environment") 61 err := e.engine.Destroy(noContext, spec) 62 if err != nil { 63 log.WithError(err). 64 Debugln("cannot destroy the pipeline environment") 65 } else { 66 log.Debugln("successfully destroyed the pipeline environment") 67 } 68 }() 69 70 if err := e.engine.Setup(noContext, spec); err != nil { 71 state.FailAll(err) 72 return e.reporter.ReportStage(noContext, state) 73 } 74 75 // create a new context with cancel in order to 76 // support fail failure when a step fails. 77 ctx, cancel := context.WithCancel(ctx) 78 defer cancel() 79 80 // create a directed graph, where each vertex in the graph 81 // is a pipeline step. 82 var d dag.Runner 83 for i := 0; i < spec.StepLen(); i++ { 84 step := spec.StepAt(i) 85 d.AddVertex(step.GetName(), func() error { 86 err := e.exec(ctx, state, spec, step) 87 // if the step is configured to fast fail the 88 // pipeline, and if the step returned a non-zero 89 // exit code, cancel the entire pipeline. 90 if step.GetErrPolicy() == ErrFailFast { 91 step := state.Find(step.GetName()) 92 // reading data from the step is not thread 93 // safe so we need to acquire a lock. 94 state.Lock() 95 exit := step.ExitCode 96 state.Unlock() 97 if exit > 0 { 98 cancel() 99 } 100 } 101 return err 102 }) 103 } 104 105 // create the vertex edges from the values configured in the 106 // depends_on attribute. 107 for i := 0; i < spec.StepLen(); i++ { 108 step := spec.StepAt(i) 109 for _, dep := range step.GetDependencies() { 110 d.AddEdge(dep, step.GetName()) 111 } 112 } 113 114 var result error 115 if err := d.Run(); err != nil { 116 switch err.Error() { 117 case "missing vertext": 118 log.Error(err) 119 case "dependency cycle detected": 120 log.Error(err) 121 } 122 result = multierror.Append(result, err) 123 124 // if the pipeline is not in a failing state, 125 // returning an unexpected error must place the 126 // pipeline in a failing state. 127 if !state.Failed() { 128 state.FailAll(err) 129 } 130 } 131 132 // once pipeline execution completes, notify the state 133 // manager that all steps are finished. 134 state.FinishAll() 135 if err := e.reporter.ReportStage(noContext, state); err != nil { 136 result = multierror.Append(result, err) 137 } 138 return result 139 } 140 141 func (e *Execer) exec(ctx context.Context, state *pipeline.State, spec Spec, step Step) error { 142 var result error 143 144 select { 145 case <-ctx.Done(): 146 state.Cancel() 147 return nil 148 default: 149 } 150 151 log := logger.FromContext(ctx) 152 log = log.WithField("step.name", step.GetName()) 153 ctx = logger.WithContext(ctx, log) 154 155 if e.sem != nil { 156 log.Trace("acquiring semaphore") 157 158 // the semaphore limits the number of steps that can run 159 // concurrently. acquire the semaphore and release when 160 // the pipeline completes. 161 err := e.sem.Acquire(ctx, 1) 162 163 // if acquiring the semaphore failed because the context 164 // deadline exceeded (e.g. the pipeline timed out) the 165 // state should be canceled. 166 switch ctx.Err() { 167 case context.Canceled, context.DeadlineExceeded: 168 log.Trace("acquiring semaphore canceled") 169 state.Cancel() 170 return nil 171 } 172 173 // if acquiring the semaphore failed for unexpected reasons 174 // the pipeline should error. 175 if err != nil { 176 log.WithError(err).Errorln("failed to acquire semaphore.") 177 return err 178 } 179 180 defer func() { 181 // recover from a panic to ensure the semaphore is 182 // released to prevent deadlock. we do not expect a 183 // panic, however, we are being overly cautious. 184 if r := recover(); r != nil { 185 // TODO(bradrydzewski) log the panic. 186 } 187 // release the semaphore 188 e.sem.Release(1) 189 log.Trace("semaphore released") 190 }() 191 } 192 193 switch { 194 case state.Cancelled(): 195 // skip if the pipeline was cancelled, either by the 196 // end user or due to timeout. 197 return nil 198 case step.GetRunPolicy() == RunNever: 199 return nil 200 case step.GetRunPolicy() == RunAlways: 201 break 202 case step.GetRunPolicy() == RunOnFailure && state.Failed() == false: 203 state.Skip(step.GetName()) 204 return e.reporter.ReportStep(noContext, state, step.GetName()) 205 case step.GetRunPolicy() == RunOnSuccess && state.Failed(): 206 state.Skip(step.GetName()) 207 return e.reporter.ReportStep(noContext, state, step.GetName()) 208 case state.Finished(step.GetName()): 209 // skip if the step if already in a finished state, 210 // for example, if the step is marked as skipped. 211 return nil 212 } 213 214 state.Start(step.GetName()) 215 err := e.reporter.ReportStep(noContext, state, step.GetName()) 216 if err != nil { 217 return err 218 } 219 220 copy := step.Clone() 221 222 // the pipeline environment variables need to be updated to 223 // reflect the current state of the build and stage. 224 state.Lock() 225 copy.SetEnviron( 226 environ.Combine( 227 copy.GetEnviron(), 228 environ.Build(state.Build), 229 environ.Stage(state.Stage), 230 environ.Step(findStep(state, step.GetName())), 231 ), 232 ) 233 state.Unlock() 234 235 // writer used to stream build logs. 236 wc := e.streamer.Stream(noContext, state, step.GetName()) 237 wc = newReplacer(wc, secretSlice(step)) 238 239 // wrap writer in extrator 240 ext := extractor.New(wc) 241 242 // if the step is configured as a daemon, it is detached 243 // from the main process and executed separately. 244 if step.IsDetached() { 245 go func() { 246 e.engine.Run(ctx, spec, copy, ext) 247 wc.Close() 248 }() 249 return nil 250 } 251 252 exited, err := e.engine.Run(ctx, spec, copy, ext) 253 254 // close the stream. If the session is a remote session, the 255 // full log buffer is uploaded to the remote server. 256 if err := wc.Close(); err != nil { 257 result = multierror.Append(result, err) 258 } 259 260 // upload card if exists 261 card, ok := ext.File() 262 if ok { 263 err = e.uploader.UploadCard(ctx, card, state, step.GetName()) 264 if err != nil { 265 log.Warnln("cannot upload card") 266 } 267 } 268 269 // if the context was cancelled and returns a Canceled or 270 // DeadlineExceeded error this indicates the pipeline was 271 // cancelled. 272 switch ctx.Err() { 273 case context.Canceled, context.DeadlineExceeded: 274 state.Cancel() 275 return nil 276 } 277 278 if exited != nil { 279 if exited.OOMKilled { 280 log.Debugln("received oom kill.") 281 state.Finish(step.GetName(), 137) 282 } else { 283 log.Debugf("received exit code %d", exited.ExitCode) 284 state.Finish(step.GetName(), exited.ExitCode) 285 } 286 err := e.reporter.ReportStep(noContext, state, step.GetName()) 287 if err != nil { 288 log.Warnln("cannot report step status.") 289 result = multierror.Append(result, err) 290 } 291 // if the exit code is 78 the system will skip all 292 // subsequent pending steps in the pipeline. 293 if exited.ExitCode == 78 { 294 log.Debugln("received exit code 78. early exit.") 295 state.SkipAll() 296 } 297 return result 298 } 299 300 switch err { 301 case context.Canceled, context.DeadlineExceeded: 302 state.Cancel() 303 return nil 304 } 305 306 // if the step failed with an internal error (as opposed to a 307 // runtime error) the step is failed. 308 state.Fail(step.GetName(), err) 309 err = e.reporter.ReportStep(noContext, state, step.GetName()) 310 if err != nil { 311 log.Warnln("cannot report step failure.") 312 result = multierror.Append(result, err) 313 } 314 return result 315 } 316 317 // helper function returns the named step from the state. 318 func findStep(state *pipeline.State, name string) *drone.Step { 319 for _, step := range state.Stage.Steps { 320 if step.Name == name { 321 return step 322 } 323 } 324 panic("step not found: " + name) 325 } 326 327 // helper function returns an array of secrets from the 328 // pipeline step. 329 func secretSlice(step Step) []Secret { 330 var secrets []Secret 331 for i := 0; i < step.GetSecretLen(); i++ { 332 secrets = append(secrets, step.GetSecretAt(i)) 333 } 334 return secrets 335 }