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  })