github.com/drone/runner-go@v1.12.0/pipeline/runtime/runner.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  	"errors"
    10  	"fmt"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/drone/runner-go/client"
    15  	"github.com/drone/runner-go/environ"
    16  	"github.com/drone/runner-go/logger"
    17  	"github.com/drone/runner-go/manifest"
    18  	"github.com/drone/runner-go/pipeline"
    19  	"github.com/drone/runner-go/secret"
    20  
    21  	"github.com/drone/drone-go/drone"
    22  	"github.com/drone/envsubst"
    23  )
    24  
    25  var noContext = context.Background()
    26  
    27  // Runner runs the pipeline.
    28  type Runner struct {
    29  	// Machine provides the runner with the name of the host
    30  	// machine executing the pipeline.
    31  	Machine string
    32  
    33  	// Environ defines global environment variables that can
    34  	// be interpolated into the yaml using bash substitution.
    35  	Environ map[string]string
    36  
    37  	// Client is the remote client responsible for interacting
    38  	// with the central server.
    39  	Client client.Client
    40  
    41  	// Compiler is responsible for compiling the pipeline
    42  	// configuration to the intermediate representation.
    43  	Compiler Compiler
    44  
    45  	// Reporter reports pipeline status and logs back to the
    46  	// remote server.
    47  	Reporter pipeline.Reporter
    48  
    49  	// Execer is responsible for executing intermediate
    50  	// representation of the pipeline and returns its results.
    51  	Exec func(context.Context, Spec, *pipeline.State) error
    52  
    53  	// Lint is responsible for linting the pipeline
    54  	// and failing if any rules are broken.
    55  	Lint func(manifest.Resource, *drone.Repo) error
    56  
    57  	// Match is an optional function that returns true if the
    58  	// repository or build match user-defined criteria. This is
    59  	// intended as a security measure to prevent a runner from
    60  	// processing an unwanted pipeline.
    61  	Match func(*drone.Repo, *drone.Build) bool
    62  
    63  	// Lookup is a helper function that extracts the resource
    64  	// from the manifest by name.
    65  	Lookup func(string, *manifest.Manifest) (manifest.Resource, error)
    66  }
    67  
    68  // Run runs the pipeline stage.
    69  func (s *Runner) Run(ctx context.Context, stage *drone.Stage) error {
    70  	log := logger.FromContext(ctx).
    71  		WithField("stage.id", stage.ID).
    72  		WithField("stage.name", stage.Name).
    73  		WithField("stage.number", stage.Number)
    74  
    75  	log.Debug("stage received")
    76  
    77  	// delivery to a single agent is not guaranteed, which means
    78  	// we need confirm receipt. The first agent that confirms
    79  	// receipt of the stage can assume ownership.
    80  
    81  	stage.Machine = s.Machine
    82  	err := s.Client.Accept(ctx, stage)
    83  	if err != nil && err == client.ErrOptimisticLock {
    84  		log.Debug("stage accepted by another runner")
    85  		return nil
    86  	}
    87  	if err != nil {
    88  		log.WithError(err).Error("cannot accept stage")
    89  		return err
    90  	}
    91  
    92  	log.Debug("stage accepted")
    93  
    94  	data, err := s.Client.Detail(ctx, stage)
    95  	if err != nil {
    96  		log.WithError(err).Error("cannot get stage details")
    97  		return err
    98  	}
    99  
   100  	log = log.WithField("repo.id", data.Repo.ID).
   101  		WithField("repo.namespace", data.Repo.Namespace).
   102  		WithField("repo.name", data.Repo.Name).
   103  		WithField("build.id", data.Build.ID).
   104  		WithField("build.number", data.Build.Number)
   105  
   106  	log.Debug("stage details fetched")
   107  	return s.run(ctx, stage, data)
   108  }
   109  
   110  // RunAccepted runs a pipeline stage that has already been
   111  // accepted and assigned to a runner.
   112  func (s *Runner) RunAccepted(ctx context.Context, id int64) error {
   113  	log := logger.FromContext(ctx).WithField("stage.id", id)
   114  	log.Debug("stage received")
   115  
   116  	data, err := s.Client.Detail(ctx, &drone.Stage{ID: id})
   117  	if err != nil {
   118  		log.WithError(err).Error("cannot get stage details")
   119  		return err
   120  	}
   121  
   122  	log = log.WithField("repo.id", data.Repo.ID).
   123  		WithField("repo.namespace", data.Repo.Namespace).
   124  		WithField("repo.name", data.Repo.Name).
   125  		WithField("build.id", data.Build.ID).
   126  		WithField("build.number", data.Build.Number)
   127  
   128  	log.Debug("stage details fetched")
   129  	return s.run(ctx, data.Stage, data)
   130  }
   131  
   132  func (s *Runner) run(ctx context.Context, stage *drone.Stage, data *client.Context) error {
   133  	log := logger.FromContext(ctx).
   134  		WithField("repo.id", data.Repo.ID).
   135  		WithField("stage.id", stage.ID).
   136  		WithField("stage.name", stage.Name).
   137  		WithField("stage.number", stage.Number).
   138  		WithField("repo.namespace", data.Repo.Namespace).
   139  		WithField("repo.name", data.Repo.Name).
   140  		WithField("build.id", data.Build.ID).
   141  		WithField("build.number", data.Build.Number)
   142  
   143  	ctxdone, cancel := context.WithCancel(ctx)
   144  	defer cancel()
   145  
   146  	timeout := time.Duration(data.Repo.Timeout) * time.Minute
   147  	ctxtimeout, cancel := context.WithTimeout(ctxdone, timeout)
   148  	defer cancel()
   149  
   150  	ctxcancel, cancel := context.WithCancel(ctxtimeout)
   151  	defer cancel()
   152  
   153  	// next we opens a connection to the server to watch for
   154  	// cancellation requests. If a build is cancelled the running
   155  	// stage should also be cancelled.
   156  	go func() {
   157  		done, _ := s.Client.Watch(ctxdone, data.Build.ID)
   158  		if done {
   159  			cancel()
   160  			log.Debugln("received cancellation")
   161  		} else {
   162  			log.Debugln("done listening for cancellations")
   163  		}
   164  	}()
   165  
   166  	envs := environ.Combine(
   167  		s.Environ,
   168  		environ.System(data.System),
   169  		environ.Repo(data.Repo),
   170  		environ.Build(data.Build),
   171  		environ.Stage(stage),
   172  		environ.Link(data.Repo, data.Build, data.System),
   173  		data.Build.Params,
   174  	)
   175  
   176  	// string substitution function ensures that string
   177  	// replacement variables are escaped and quoted if they
   178  	// contain a newline character.
   179  	subf := func(k string) string {
   180  		v := envs[k]
   181  		if strings.Contains(v, "\n") {
   182  			v = fmt.Sprintf("%q", v)
   183  		}
   184  		return v
   185  	}
   186  
   187  	state := &pipeline.State{
   188  		Build:  data.Build,
   189  		Stage:  stage,
   190  		Repo:   data.Repo,
   191  		System: data.System,
   192  	}
   193  
   194  	// evaluates whether or not the agent can process the
   195  	// pipeline. An agent may choose to reject a repository
   196  	// or build for security reasons.
   197  	if s.Match != nil && s.Match(data.Repo, data.Build) == false {
   198  		log.Error("cannot process stage, access denied")
   199  		state.FailAll(errors.New("insufficient permission to run the pipeline"))
   200  		return s.Reporter.ReportStage(noContext, state)
   201  	}
   202  
   203  	// evaluates string replacement expressions and returns an
   204  	// update configuration file string.
   205  	config, err := envsubst.Eval(string(data.Config.Data), subf)
   206  	if err != nil {
   207  		log.WithError(err).Error("cannot emulate bash substitution")
   208  		state.FailAll(err)
   209  		return s.Reporter.ReportStage(noContext, state)
   210  	}
   211  
   212  	// parse the yaml configuration file.
   213  	manifest, err := manifest.ParseString(config)
   214  	if err != nil {
   215  		log.WithError(err).Error("cannot parse configuration file")
   216  		state.FailAll(err)
   217  		return s.Reporter.ReportStage(noContext, state)
   218  	}
   219  
   220  	// find the named stage in the yaml configuration file.
   221  	resource, err := s.Lookup(stage.Name, manifest)
   222  	if err != nil {
   223  		log.WithError(err).Error("cannot find pipeline resource")
   224  		state.FailAll(err)
   225  		return s.Reporter.ReportStage(noContext, state)
   226  	}
   227  
   228  	// lint the pipeline configuration and fail the build
   229  	// if any linting rules are broken.
   230  	err = s.Lint(resource, data.Repo)
   231  	if err != nil {
   232  		log.WithError(err).Error("cannot accept configuration")
   233  		state.FailAll(err)
   234  		return s.Reporter.ReportStage(noContext, state)
   235  	}
   236  
   237  	secrets := secret.Combine(
   238  		secret.Static(data.Secrets),
   239  		secret.Encrypted(),
   240  	)
   241  
   242  	// compile the yaml configuration file to an intermediate
   243  	// representation, and then
   244  	args := CompilerArgs{
   245  		Pipeline: resource,
   246  		Manifest: manifest,
   247  		Build:    data.Build,
   248  		Stage:    stage,
   249  		Repo:     data.Repo,
   250  		System:   data.System,
   251  		Netrc:    data.Netrc,
   252  		Secret:   secrets,
   253  	}
   254  
   255  	spec := s.Compiler.Compile(ctx, args)
   256  	for i := 0; i < spec.StepLen(); i++ {
   257  		src := spec.StepAt(i)
   258  
   259  		// steps that are skipped are ignored and are not stored
   260  		// in the drone database, nor displayed in the UI.
   261  		if src.GetRunPolicy() == RunNever {
   262  			continue
   263  		}
   264  		stage.Steps = append(stage.Steps, &drone.Step{
   265  			Name:      src.GetName(),
   266  			Number:    len(stage.Steps) + 1,
   267  			StageID:   stage.ID,
   268  			Status:    drone.StatusPending,
   269  			ErrIgnore: src.GetErrPolicy() == ErrIgnore,
   270  			Image:     src.GetImage(),
   271  			Detached:  src.IsDetached(),
   272  			DependsOn: src.GetDependencies(),
   273  		})
   274  	}
   275  
   276  	stage.Started = time.Now().Unix()
   277  	stage.Status = drone.StatusRunning
   278  	if err := s.Client.Update(ctx, stage); err != nil {
   279  		log.WithError(err).Error("cannot update stage")
   280  		return err
   281  	}
   282  
   283  	log.Debug("updated stage to running")
   284  
   285  	ctxlogger := logger.WithContext(ctxcancel, log)
   286  	err = s.Exec(ctxlogger, spec, state)
   287  	if err != nil {
   288  		log.WithError(err).
   289  			WithField("duration", stage.Stopped-stage.Started).
   290  			Debug("stage failed")
   291  		return err
   292  	}
   293  	log.WithField("duration", stage.Stopped-stage.Started).
   294  		Debug("updated stage to complete")
   295  	return nil
   296  }