github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/atc/worker/container_test.go (about) 1 package worker_test 2 3 import ( 4 "context" 5 "errors" 6 "io" 7 "io/ioutil" 8 9 "code.cloudfoundry.org/garden" 10 "code.cloudfoundry.org/garden/gardenfakes" 11 "code.cloudfoundry.org/lager" 12 "github.com/pf-qiu/concourse/v6/atc/db" 13 "github.com/pf-qiu/concourse/v6/atc/db/dbfakes" 14 "github.com/pf-qiu/concourse/v6/atc/runtime" 15 "github.com/pf-qiu/concourse/v6/atc/worker" 16 "github.com/pf-qiu/concourse/v6/atc/worker/gclient/gclientfakes" 17 "github.com/pf-qiu/concourse/v6/atc/worker/workerfakes" 18 "github.com/onsi/gomega/gbytes" 19 20 . "github.com/onsi/ginkgo" 21 . "github.com/onsi/gomega" 22 ) 23 24 var _ = Describe("RunScript", func() { 25 var ( 26 testLogger lager.Logger 27 28 fakeGardenContainerScriptStdout string 29 fakeGardenContainerScriptStderr string 30 scriptExitStatus int 31 32 runErr error 33 attachErr error 34 runScriptErr error 35 36 scriptProcess *gardenfakes.FakeProcess 37 38 stderrBuf *gbytes.Buffer 39 40 fakeGClientContainer *gclientfakes.FakeContainer 41 fakeGClient *gclientfakes.FakeClient 42 fakeVolumeClient *workerfakes.FakeVolumeClient 43 fakeDBVolumeRepository *dbfakes.FakeVolumeRepository 44 fakeImageFactory *workerfakes.FakeImageFactory 45 fakeFetcher *workerfakes.FakeFetcher 46 fakeDBTeamFactory *dbfakes.FakeTeamFactory 47 fakeDBWorker *dbfakes.FakeWorker 48 fakeCreatedContainer *dbfakes.FakeCreatedContainer 49 fakeResourceCacheFactory *dbfakes.FakeResourceCacheFactory 50 51 gardenWorker worker.Worker 52 workerContainer worker.Container 53 fakeOwner *dbfakes.FakeContainerOwner 54 55 runScriptCtx context.Context 56 runScriptCancel func() 57 58 runScriptBinPath string 59 runScriptArgs []string 60 runScriptInput []byte 61 runScriptOutput map[string]string 62 runScriptLogDestination io.Writer 63 runScriptRecoverable bool 64 ) 65 66 BeforeEach(func() { 67 testLogger = lager.NewLogger("test-logger") 68 fakeDBVolumeRepository = new(dbfakes.FakeVolumeRepository) 69 fakeGClientContainer = new(gclientfakes.FakeContainer) 70 fakeCreatedContainer = new(dbfakes.FakeCreatedContainer) 71 fakeGClient = new(gclientfakes.FakeClient) 72 fakeVolumeClient = new(workerfakes.FakeVolumeClient) 73 fakeImageFactory = new(workerfakes.FakeImageFactory) 74 fakeFetcher = new(workerfakes.FakeFetcher) 75 fakeDBTeamFactory = new(dbfakes.FakeTeamFactory) 76 fakeDBWorker = new(dbfakes.FakeWorker) 77 fakeResourceCacheFactory = new(dbfakes.FakeResourceCacheFactory) 78 79 fakeOwner = new(dbfakes.FakeContainerOwner) 80 81 stderrBuf = gbytes.NewBuffer() 82 83 fakeGardenContainerScriptStdout = "{}" 84 fakeGardenContainerScriptStderr = "" 85 scriptExitStatus = 0 86 87 runErr = nil 88 attachErr = nil 89 90 scriptProcess = new(gardenfakes.FakeProcess) 91 scriptProcess.IDReturns("some-proc-id") 92 scriptProcess.WaitStub = func() (int, error) { 93 return scriptExitStatus, nil 94 } 95 96 gardenWorker = worker.NewGardenWorker( 97 fakeGClient, 98 fakeDBVolumeRepository, 99 fakeVolumeClient, 100 fakeImageFactory, 101 fakeFetcher, 102 fakeDBTeamFactory, 103 fakeDBWorker, 104 fakeResourceCacheFactory, 105 0, 106 ) 107 108 fakeCreatedContainer.HandleReturns("some-handle") 109 fakeDBWorker.FindContainerReturns(nil, fakeCreatedContainer, nil) 110 fakeGClient.LookupReturns(fakeGClientContainer, nil) 111 112 workerContainer, _ = gardenWorker.FindOrCreateContainer( 113 context.TODO(), 114 testLogger, 115 fakeOwner, 116 db.ContainerMetadata{}, 117 worker.ContainerSpec{}, 118 ) 119 120 runScriptCtx, runScriptCancel = context.WithCancel(context.Background()) 121 122 runScriptBinPath = "some-bin-path" 123 runScriptArgs = []string{"arg-1", "some-arg2"} 124 runScriptInput = []byte(`{ 125 "source": {"some":"source"}, 126 "params": {"some":"params"}, 127 "version": {"some":"version"} 128 }`) 129 runScriptOutput = make(map[string]string) 130 runScriptLogDestination = stderrBuf 131 runScriptRecoverable = true 132 133 }) 134 135 Context("running", func() { 136 BeforeEach(func() { 137 fakeGClientContainer.RunStub = func(ctx context.Context, spec garden.ProcessSpec, io garden.ProcessIO) (garden.Process, error) { 138 if runErr != nil { 139 return nil, runErr 140 } 141 142 _, err := io.Stdout.Write([]byte(fakeGardenContainerScriptStdout)) 143 Expect(err).NotTo(HaveOccurred()) 144 145 _, err = io.Stderr.Write([]byte(fakeGardenContainerScriptStderr)) 146 Expect(err).NotTo(HaveOccurred()) 147 148 return scriptProcess, nil 149 } 150 151 fakeGClientContainer.AttachStub = func(ctx context.Context, pid string, io garden.ProcessIO) (garden.Process, error) { 152 if attachErr != nil { 153 return nil, attachErr 154 } 155 156 _, err := io.Stdout.Write([]byte(fakeGardenContainerScriptStdout)) 157 Expect(err).NotTo(HaveOccurred()) 158 159 _, err = io.Stderr.Write([]byte(fakeGardenContainerScriptStderr)) 160 Expect(err).NotTo(HaveOccurred()) 161 162 return scriptProcess, nil 163 } 164 }) 165 166 JustBeforeEach(func() { 167 runScriptErr = workerContainer.RunScript( 168 runScriptCtx, 169 runScriptBinPath, 170 runScriptArgs, 171 runScriptInput, 172 &runScriptOutput, 173 runScriptLogDestination, 174 runScriptRecoverable, 175 ) 176 }) 177 178 Context("when a result is already present on the container", func() { 179 BeforeEach(func() { 180 fakeGClientContainer.PropertiesReturns(garden.Properties{"concourse:resource-result": `{"some-foo-key": "some-foo-value"}`}, nil) 181 }) 182 183 It("exits successfully", func() { 184 Expect(runScriptErr).NotTo(HaveOccurred()) 185 }) 186 187 It("does not run or attach to anything", func() { 188 Expect(fakeGClientContainer.RunCallCount()).To(BeZero()) 189 Expect(fakeGClientContainer.AttachCallCount()).To(BeZero()) 190 }) 191 192 It("can be accessed on the RunScript Output", func() { 193 Expect(runScriptOutput).To(HaveKeyWithValue("some-foo-key", "some-foo-value")) 194 }) 195 }) 196 197 Context("when the process has already been spawned", func() { 198 BeforeEach(func() { 199 runScriptInput = []byte(`{ 200 "bar-key": {"baz":"yarp"} 201 }`) 202 fakeGClientContainer.PropertiesReturns(nil, nil) 203 }) 204 205 It("reattaches to it", func() { 206 Expect(fakeGClientContainer.AttachCallCount()).To(Equal(1)) 207 208 _, pid, io := fakeGClientContainer.AttachArgsForCall(0) 209 Expect(pid).To(Equal(runtime.ResourceProcessID)) 210 211 // send request on stdin in case process hasn't read it yet 212 request, err := ioutil.ReadAll(io.Stdin) 213 Expect(err).NotTo(HaveOccurred()) 214 215 Expect(request).To(MatchJSON(`{ 216 "bar-key": {"baz":"yarp"} 217 }`)) 218 }) 219 220 It("does not run an additional process", func() { 221 Expect(fakeGClientContainer.RunCallCount()).To(BeZero()) 222 }) 223 224 Context("when the process prints a response", func() { 225 BeforeEach(func() { 226 fakeGardenContainerScriptStdout = `{"some-key":"with-some-value"}` 227 }) 228 229 It("can be accessed on the RunScript Output", func() { 230 Expect(runScriptOutput).To(HaveKeyWithValue("some-key", "with-some-value")) 231 232 }) 233 234 It("saves it as a property on the container", func() { 235 Expect(fakeGClientContainer.SetPropertyCallCount()).To(Equal(1)) 236 237 name, value := fakeGClientContainer.SetPropertyArgsForCall(0) 238 Expect(name).To(Equal("concourse:resource-result")) 239 Expect(value).To(Equal(fakeGardenContainerScriptStdout)) 240 }) 241 }) 242 243 Context("when the process outputs to stderr", func() { 244 BeforeEach(func() { 245 fakeGardenContainerScriptStderr = "some stderr data" 246 }) 247 248 It("emits it to the log sink", func() { 249 Expect(runScriptLogDestination).To(gbytes.Say("some stderr data")) 250 }) 251 }) 252 253 Context("when attaching to the process fails", func() { 254 disaster := errors.New("oh no!") 255 256 BeforeEach(func() { 257 attachErr = disaster 258 }) 259 260 Context("and run succeeds", func() { 261 It("succeeds", func() { 262 Expect(runScriptErr).ToNot(HaveOccurred()) 263 }) 264 }) 265 266 Context("and run subsequently fails", func() { 267 BeforeEach(func() { 268 runErr = disaster 269 }) 270 271 It("errors", func() { 272 Expect(runScriptErr).To(Equal(disaster)) 273 }) 274 }) 275 }) 276 277 Context("when the process exits nonzero", func() { 278 BeforeEach(func() { 279 scriptExitStatus = 9 280 }) 281 282 It("returns an err containing stdout/stderr of the process", func() { 283 Expect(runScriptErr).To(HaveOccurred()) 284 Expect(runScriptErr.Error()).To(ContainSubstring("exit status 9")) 285 }) 286 }) 287 288 }) 289 290 Context("when the process has not yet been spawned", func() { 291 BeforeEach(func() { 292 fakeGClientContainer.PropertiesReturns(nil, nil) 293 attachErr = errors.New("not found") 294 }) 295 296 It("specifies the process id in the process spec", func() { 297 Expect(fakeGClientContainer.RunCallCount()).To(Equal(1)) 298 299 _, spec, _ := fakeGClientContainer.RunArgsForCall(0) 300 Expect(spec.ID).To(Equal(runtime.ResourceProcessID)) 301 }) 302 303 It("runs the process using <destination (args[0])> with the request as args to stdin", func() { 304 Expect(fakeGClientContainer.RunCallCount()).To(Equal(1)) 305 306 _, spec, io := fakeGClientContainer.RunArgsForCall(0) 307 Expect(spec.Path).To(Equal(runScriptBinPath)) 308 Expect(spec.Args).To(ConsistOf(runScriptArgs)) 309 310 request, err := ioutil.ReadAll(io.Stdin) 311 Expect(err).NotTo(HaveOccurred()) 312 313 Expect(request).To(MatchJSON(`{ 314 "source": {"some":"source"}, 315 "params": {"some":"params"}, 316 "version": {"some":"version"} 317 }`)) 318 }) 319 320 Context("when process prints the response", func() { 321 BeforeEach(func() { 322 fakeGardenContainerScriptStdout = `{ 323 "version": {"some": "new-version"}, 324 "metadata": [ 325 {"name": "a", "value":"a-value"}, 326 {"name": "b","value": "b-value"} 327 ] 328 }` 329 }) 330 331 // It("can be accessed on the versioned source", func() { 332 // Expect(versionedSource.Version()).To(Equal(atc.Version{"some": "new-version"})) 333 // Expect(versionedSource.Metadata()).To(Equal([]atc.MetadataField{ 334 // {Name: "a", Value: "a-value"}, 335 // {Name: "b", Value: "b-value"}, 336 // })) 337 // }) 338 339 It("saves it as a property on the container", func() { 340 Expect(fakeGClientContainer.SetPropertyCallCount()).To(Equal(1)) 341 342 name, value := fakeGClientContainer.SetPropertyArgsForCall(0) 343 Expect(name).To(Equal("concourse:resource-result")) 344 Expect(value).To(Equal(fakeGardenContainerScriptStdout)) 345 }) 346 }) 347 348 Context("when process outputs to stderr", func() { 349 BeforeEach(func() { 350 fakeGardenContainerScriptStderr = "some stderr data" 351 }) 352 353 It("emits it to the log sink", func() { 354 Expect(stderrBuf).To(gbytes.Say("some stderr data")) 355 }) 356 }) 357 358 Context("when running process fails", func() { 359 disaster := errors.New("oh no!") 360 361 BeforeEach(func() { 362 runErr = disaster 363 }) 364 365 It("returns an err", func() { 366 Expect(runScriptErr).To(HaveOccurred()) 367 Expect(runScriptErr).To(Equal(disaster)) 368 }) 369 }) 370 371 Context("when process exits nonzero", func() { 372 BeforeEach(func() { 373 scriptExitStatus = 9 374 }) 375 376 It("returns an err containing stdout/stderr of the process", func() { 377 Expect(runScriptErr).To(HaveOccurred()) 378 Expect(runScriptErr.Error()).To(ContainSubstring("exit status 9")) 379 }) 380 }) 381 382 Context("when the process stdout is malformed", func() { 383 BeforeEach(func() { 384 fakeGardenContainerScriptStdout = "ß" 385 }) 386 387 It("returns an error", func() { 388 Expect(runScriptErr).To(HaveOccurred()) 389 }) 390 391 It("returns original payload in error", func() { 392 Expect(runScriptErr.Error()).Should(ContainSubstring(fakeGardenContainerScriptStdout)) 393 }) 394 }) 395 }) 396 }) 397 398 Context("when canceling the context", func() { 399 var waited chan<- struct{} 400 var done chan struct{} 401 402 BeforeEach(func() { 403 fakeGClientContainer.AttachReturns(nil, errors.New("not-found")) 404 fakeGClientContainer.RunReturns(scriptProcess, nil) 405 fakeGClientContainer.PropertyReturns("", errors.New("nope")) 406 407 waiting := make(chan struct{}) 408 done = make(chan struct{}) 409 waited = waiting 410 411 scriptProcess.WaitStub = func() (int, error) { 412 // cause waiting to block so that it can be aborted 413 <-waiting 414 return 0, nil 415 } 416 417 fakeGClientContainer.StopStub = func(bool) error { 418 close(waited) 419 return nil 420 } 421 422 go func() { 423 runScriptErr = workerContainer.RunScript( 424 runScriptCtx, 425 runScriptBinPath, 426 runScriptArgs, 427 runScriptInput, 428 &runScriptOutput, 429 runScriptLogDestination, 430 runScriptRecoverable, 431 ) 432 433 close(done) 434 }() 435 }) 436 437 It("stops the container", func() { 438 runScriptCancel() 439 <-done 440 Expect(fakeGClientContainer.StopCallCount()).To(Equal(1)) 441 isStopped := fakeGClientContainer.StopArgsForCall(0) 442 Expect(isStopped).To(BeFalse()) 443 }) 444 445 It("doesn't send garden terminate signal to process", func() { 446 runScriptCancel() 447 <-done 448 Expect(runScriptErr).To(Equal(context.Canceled)) 449 Expect(scriptProcess.SignalCallCount()).To(BeZero()) 450 }) 451 452 Context("when container.stop returns an error", func() { 453 var disaster error 454 455 BeforeEach(func() { 456 disaster = errors.New("gotta get away") 457 458 fakeGClientContainer.StopStub = func(bool) error { 459 close(waited) 460 return disaster 461 } 462 }) 463 464 It("masks the error", func() { 465 runScriptCancel() 466 <-done 467 Expect(runScriptErr).To(Equal(context.Canceled)) 468 }) 469 }) 470 }) 471 })