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 }