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  }