github.com/cloudfoundry-attic/ltc@v0.0.0-20151123212628-098adc7919fc/droplet_runner/droplet_runner_test.go (about)

     1  package droplet_runner_test
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"reflect"
    11  	"strings"
    12  	"time"
    13  
    14  	. "github.com/onsi/ginkgo"
    15  	. "github.com/onsi/gomega"
    16  
    17  	"github.com/cloudfoundry-incubator/bbs/models"
    18  	"github.com/cloudfoundry-incubator/ltc/app_examiner"
    19  	"github.com/cloudfoundry-incubator/ltc/app_examiner/fake_app_examiner"
    20  	"github.com/cloudfoundry-incubator/ltc/app_runner"
    21  	"github.com/cloudfoundry-incubator/ltc/app_runner/fake_app_runner"
    22  	"github.com/cloudfoundry-incubator/ltc/blob_store/blob"
    23  	"github.com/cloudfoundry-incubator/ltc/config/persister"
    24  	"github.com/cloudfoundry-incubator/ltc/droplet_runner"
    25  	"github.com/cloudfoundry-incubator/ltc/droplet_runner/fake_blob_store"
    26  	"github.com/cloudfoundry-incubator/ltc/droplet_runner/fake_proxyconf_reader"
    27  	"github.com/cloudfoundry-incubator/ltc/task_runner/fake_task_runner"
    28  	"github.com/cloudfoundry-incubator/ltc/test_helpers/matchers"
    29  
    30  	config_package "github.com/cloudfoundry-incubator/ltc/config"
    31  )
    32  
    33  var _ = Describe("DropletRunner", func() {
    34  	var (
    35  		fakeAppRunner       *fake_app_runner.FakeAppRunner
    36  		fakeTaskRunner      *fake_task_runner.FakeTaskRunner
    37  		config              *config_package.Config
    38  		fakeBlobStore       *fake_blob_store.FakeBlobStore
    39  		fakeAppExaminer     *fake_app_examiner.FakeAppExaminer
    40  		fakeProxyConfReader *fake_proxyconf_reader.FakeProxyConfReader
    41  		dropletRunner       droplet_runner.DropletRunner
    42  	)
    43  
    44  	BeforeEach(func() {
    45  		fakeAppRunner = &fake_app_runner.FakeAppRunner{}
    46  		fakeTaskRunner = &fake_task_runner.FakeTaskRunner{}
    47  		config = config_package.New(persister.NewMemPersister())
    48  		fakeBlobStore = &fake_blob_store.FakeBlobStore{}
    49  		fakeAppExaminer = &fake_app_examiner.FakeAppExaminer{}
    50  		fakeProxyConfReader = &fake_proxyconf_reader.FakeProxyConfReader{}
    51  		dropletRunner = droplet_runner.New(fakeAppRunner, fakeTaskRunner, config, fakeBlobStore, fakeAppExaminer, fakeProxyConfReader)
    52  	})
    53  
    54  	Describe("ListDroplets", func() {
    55  		It("returns a list of droplets in the blob store", func() {
    56  			fakeBlobStore.ListReturns([]blob.Blob{
    57  				{Path: "X-bits.zip", Created: time.Unix(1000, 0), Size: 100},
    58  				{Path: "X-droplet.tgz", Created: time.Unix(2000, 0), Size: 200},
    59  				{Path: "Y-bits.zip"},
    60  				{Path: "droplet.tgz"},
    61  			}, nil)
    62  
    63  			Expect(dropletRunner.ListDroplets()).To(Equal([]droplet_runner.Droplet{
    64  				{Name: "X", Created: time.Unix(2000, 0), Size: 200},
    65  			}))
    66  		})
    67  
    68  		It("returns an error when querying the blob store fails", func() {
    69  			fakeBlobStore.ListReturns(nil, errors.New("some error"))
    70  
    71  			_, err := dropletRunner.ListDroplets()
    72  			Expect(err).To(MatchError("some error"))
    73  		})
    74  	})
    75  
    76  	Describe("UploadBits", func() {
    77  		Context("when the archive path is a file and exists", func() {
    78  			var tmpFile *os.File
    79  
    80  			BeforeEach(func() {
    81  				tmpDir := os.TempDir()
    82  				var err error
    83  				tmpFile, err = ioutil.TempFile(tmpDir, "tmp_file")
    84  				Expect(err).NotTo(HaveOccurred())
    85  
    86  				Expect(ioutil.WriteFile(tmpFile.Name(), []byte("some contents"), 0600)).To(Succeed())
    87  			})
    88  
    89  			AfterEach(func() {
    90  				tmpFile.Close()
    91  				Expect(os.Remove(tmpFile.Name())).To(Succeed())
    92  			})
    93  
    94  			It("uploads the file to the bucket", func() {
    95  				Expect(dropletRunner.UploadBits("droplet-name", tmpFile.Name())).To(Succeed())
    96  
    97  				Expect(fakeBlobStore.UploadCallCount()).To(Equal(1))
    98  				path, _ := fakeBlobStore.UploadArgsForCall(0)
    99  				Expect(path).To(Equal("droplet-name-bits.zip"))
   100  			})
   101  
   102  			It("returns an error when we fail to open the droplet bits", func() {
   103  				err := dropletRunner.UploadBits("droplet-name", "some non-existent file")
   104  				Expect(reflect.TypeOf(err).String()).To(Equal("*os.PathError"))
   105  			})
   106  
   107  			It("returns an error when the upload fails", func() {
   108  				fakeBlobStore.UploadReturns(errors.New("some error"))
   109  
   110  				err := dropletRunner.UploadBits("droplet-name", tmpFile.Name())
   111  				Expect(err).To(MatchError("some error"))
   112  			})
   113  		})
   114  	})
   115  
   116  	Describe("BuildDroplet", func() {
   117  		It("does the build droplet task", func() {
   118  			config.SetBlobStore("blob-host", "7474", "dav-user", "dav-pass")
   119  			Expect(config.Save()).To(Succeed())
   120  
   121  			blobURL := fmt.Sprintf("http://%s:%s@%s:%s%s",
   122  				config.BlobStore().Username,
   123  				config.BlobStore().Password,
   124  				config.BlobStore().Host,
   125  				config.BlobStore().Port,
   126  				"/blobs/droplet-name")
   127  
   128  			fakeBlobStore.DownloadAppBitsActionReturns(models.WrapAction(&models.DownloadAction{
   129  				From: blobURL + "-bits.zip",
   130  				To:   "/tmp/app",
   131  				User: "vcap",
   132  			}))
   133  
   134  			fakeBlobStore.DeleteAppBitsActionReturns(models.WrapAction(&models.RunAction{
   135  				Path: "/tmp/davtool",
   136  				Dir:  "/",
   137  				Args: []string{"delete", blobURL + "-bits.zip"},
   138  				User: "vcap",
   139  			}))
   140  
   141  			fakeBlobStore.UploadDropletActionReturns(models.WrapAction(&models.RunAction{
   142  				Path: "/tmp/davtool",
   143  				Dir:  "/",
   144  				Args: []string{"put", blobURL + "-droplet.tgz", "/tmp/droplet"},
   145  				User: "vcap",
   146  			}))
   147  			err := dropletRunner.BuildDroplet("task-name", "droplet-name", "buildpack", map[string]string{}, 128, 100, 800)
   148  			Expect(err).NotTo(HaveOccurred())
   149  
   150  			Expect(fakeTaskRunner.CreateTaskCallCount()).To(Equal(1))
   151  			createTaskParams := fakeTaskRunner.CreateTaskArgsForCall(0)
   152  			Expect(createTaskParams).ToNot(BeNil())
   153  			receptorRequest := createTaskParams.GetReceptorRequest()
   154  
   155  			expectedActions := models.WrapAction(&models.SerialAction{
   156  				Actions: []*models.Action{
   157  					models.WrapAction(&models.DownloadAction{
   158  						From: "http://file-server.service.cf.internal:8080/v1/static/cell-helpers/cell-helpers.tgz",
   159  						To:   "/tmp",
   160  						User: "vcap",
   161  					}),
   162  					models.WrapAction(&models.DownloadAction{
   163  						From: "http://file-server.service.cf.internal:8080/v1/static/buildpack_app_lifecycle/buildpack_app_lifecycle.tgz",
   164  						To:   "/tmp",
   165  						User: "vcap",
   166  					}),
   167  					models.WrapAction(&models.DownloadAction{
   168  						From: blobURL + "-bits.zip",
   169  						To:   "/tmp/app",
   170  						User: "vcap",
   171  					}),
   172  					models.WrapAction(&models.RunAction{
   173  						Path: "/tmp/davtool",
   174  						Dir:  "/",
   175  						Args: []string{"delete", blobURL + "-bits.zip"},
   176  						User: "vcap",
   177  					}),
   178  					models.WrapAction(&models.RunAction{
   179  						Path: "/bin/chmod",
   180  						Dir:  "/tmp/app",
   181  						Args: []string{"-R", "a+X", "."},
   182  						User: "vcap",
   183  					}),
   184  					models.WrapAction(&models.RunAction{
   185  						Path: "/tmp/builder",
   186  						Dir:  "/",
   187  						Args: []string{
   188  							"-buildArtifactsCacheDir=/tmp/cache",
   189  							"-buildDir=/tmp/app",
   190  							"-buildpackOrder=buildpack",
   191  							"-buildpacksDir=/tmp/buildpacks",
   192  							"-outputBuildArtifactsCache=/tmp/output-cache",
   193  							"-outputDroplet=/tmp/droplet",
   194  							"-outputMetadata=/tmp/result.json",
   195  							"-skipCertVerify=false",
   196  							"-skipDetect=true",
   197  						},
   198  						User: "vcap",
   199  					}),
   200  					models.WrapAction(&models.RunAction{
   201  						Path: "/tmp/davtool",
   202  						Dir:  "/",
   203  						Args: []string{"put", blobURL + "-droplet.tgz", "/tmp/droplet"},
   204  						User: "vcap",
   205  					}),
   206  				},
   207  			})
   208  			Expect(receptorRequest.Action).To(Equal(expectedActions))
   209  			Expect(receptorRequest.TaskGuid).To(Equal("task-name"))
   210  			Expect(receptorRequest.LogGuid).To(Equal("task-name"))
   211  			Expect(receptorRequest.MetricsGuid).To(Equal("task-name"))
   212  			Expect(receptorRequest.RootFS).To(Equal("preloaded:cflinuxfs2"))
   213  			Expect(receptorRequest.EnvironmentVariables).To(matchers.ContainExactly([]*models.EnvironmentVariable{
   214  				{Name: "CF_STACK", Value: "cflinuxfs2"},
   215  				{Name: "MEMORY_LIMIT", Value: "128M"},
   216  				{Name: "http_proxy", Value: ""},
   217  				{Name: "https_proxy", Value: ""},
   218  				{Name: "no_proxy", Value: ""},
   219  			}))
   220  			Expect(receptorRequest.LogSource).To(Equal("BUILD"))
   221  			Expect(receptorRequest.Domain).To(Equal("lattice"))
   222  			Expect(receptorRequest.Privileged).To(BeTrue())
   223  			Expect(receptorRequest.EgressRules).ToNot(BeNil())
   224  			Expect(receptorRequest.EgressRules).To(BeEmpty())
   225  			Expect(receptorRequest.MemoryMB).To(Equal(128))
   226  		})
   227  
   228  		It("passes through user environment variables", func() {
   229  			config.SetBlobStore("blob-host", "7474", "dav-user", "dav-pass")
   230  			Expect(config.Save()).To(Succeed())
   231  
   232  			env := map[string]string{
   233  				"ENV_VAR":   "stuff",
   234  				"OTHER_VAR": "same",
   235  			}
   236  
   237  			err := dropletRunner.BuildDroplet("task-name", "droplet-name", "buildpack", env, 128, 100, 800)
   238  			Expect(err).NotTo(HaveOccurred())
   239  
   240  			Expect(fakeTaskRunner.CreateTaskCallCount()).To(Equal(1))
   241  			createTaskParams := fakeTaskRunner.CreateTaskArgsForCall(0)
   242  			Expect(createTaskParams).ToNot(BeNil())
   243  			receptorRequest := createTaskParams.GetReceptorRequest()
   244  
   245  			Expect(receptorRequest.EnvironmentVariables).To(matchers.ContainExactly([]*models.EnvironmentVariable{
   246  				{Name: "CF_STACK", Value: "cflinuxfs2"},
   247  				{Name: "MEMORY_LIMIT", Value: "128M"},
   248  				{Name: "ENV_VAR", Value: "stuff"},
   249  				{Name: "OTHER_VAR", Value: "same"},
   250  				{Name: "http_proxy", Value: ""},
   251  				{Name: "https_proxy", Value: ""},
   252  				{Name: "no_proxy", Value: ""},
   253  			}))
   254  		})
   255  
   256  		It("sets proxy environment variables if a proxyconf exists", func() {
   257  			config.SetBlobStore("blob-host", "7474", "dav-user", "dav-pass")
   258  			Expect(config.Save()).To(Succeed())
   259  
   260  			env := map[string]string{}
   261  
   262  			fakeProxyConfReader.ProxyConfReturns(droplet_runner.ProxyConf{
   263  				HTTPProxy:  "http://proxy",
   264  				HTTPSProxy: "https://proxy",
   265  				NoProxy:    "no-proxy",
   266  			}, nil)
   267  
   268  			err := dropletRunner.BuildDroplet("task-name", "droplet-name", "buildpack", env, 128, 100, 800)
   269  			Expect(err).NotTo(HaveOccurred())
   270  
   271  			Expect(fakeTaskRunner.CreateTaskCallCount()).To(Equal(1))
   272  			createTaskParams := fakeTaskRunner.CreateTaskArgsForCall(0)
   273  			Expect(createTaskParams).ToNot(BeNil())
   274  			receptorRequest := createTaskParams.GetReceptorRequest()
   275  
   276  			Expect(receptorRequest.EnvironmentVariables).To(matchers.ContainExactly([]*models.EnvironmentVariable{
   277  				{Name: "CF_STACK", Value: "cflinuxfs2"},
   278  				{Name: "MEMORY_LIMIT", Value: "128M"},
   279  				{Name: "http_proxy", Value: "http://proxy"},
   280  				{Name: "https_proxy", Value: "https://proxy"},
   281  				{Name: "no_proxy", Value: "no-proxy"},
   282  			}))
   283  		})
   284  
   285  		It("passes through cpuWeight, memoryMB and diskMB", func() {
   286  			config.SetBlobStore("blob-host", "7474", "dav-user", "dav-pass")
   287  			Expect(config.Save()).To(Succeed())
   288  
   289  			err := dropletRunner.BuildDroplet("task-name", "droplet-name", "buildpack", map[string]string{}, 1, 2, 3)
   290  			Expect(err).NotTo(HaveOccurred())
   291  
   292  			Expect(fakeTaskRunner.CreateTaskCallCount()).To(Equal(1))
   293  			createTaskParams := fakeTaskRunner.CreateTaskArgsForCall(0)
   294  			receptorRequest := createTaskParams.GetReceptorRequest()
   295  			Expect(receptorRequest.MemoryMB).To(Equal(1))
   296  			Expect(receptorRequest.CPUWeight).To(Equal(uint(2)))
   297  			Expect(receptorRequest.DiskMB).To(Equal(3))
   298  		})
   299  
   300  		It("returns an error when ProxyConfReader fails", func() {
   301  			fakeProxyConfReader.ProxyConfReturns(droplet_runner.ProxyConf{}, errors.New("can't proxy"))
   302  
   303  			err := dropletRunner.BuildDroplet("task-name", "droplet-name", "buildpack", map[string]string{}, 0, 0, 0)
   304  			Expect(err).To(MatchError("can't proxy"))
   305  
   306  			Expect(fakeTaskRunner.CreateTaskCallCount()).To(Equal(0))
   307  		})
   308  
   309  		It("returns an error when create task fails", func() {
   310  			fakeTaskRunner.CreateTaskReturns(errors.New("creating task failed"))
   311  
   312  			err := dropletRunner.BuildDroplet("task-name", "droplet-name", "buildpack", map[string]string{}, 0, 0, 0)
   313  			Expect(err).To(MatchError("creating task failed"))
   314  
   315  			Expect(fakeTaskRunner.CreateTaskCallCount()).To(Equal(1))
   316  		})
   317  	})
   318  
   319  	Describe("LaunchDroplet", func() {
   320  		BeforeEach(func() {
   321  			config.SetBlobStore("blob-host", "7474", "dav-user", "dav-pass")
   322  			Expect(config.Save()).To(Succeed())
   323  		})
   324  
   325  		It("launches the droplet lrp task with a start command from buildpack results", func() {
   326  			fakeBlobStore.DownloadDropletActionReturns(models.WrapAction(&models.DownloadAction{
   327  				From: "http://dav-user:dav-pass@blob-host:7474/blobs/droplet-name-droplet.tgz",
   328  				To:   "/home/vcap",
   329  				User: "vcap",
   330  			}))
   331  
   332  			err := dropletRunner.LaunchDroplet("app-name", "droplet-name", "", []string{}, app_runner.AppEnvironmentParams{})
   333  			Expect(err).NotTo(HaveOccurred())
   334  
   335  			Expect(fakeAppRunner.CreateAppCallCount()).To(Equal(1))
   336  			createAppParams := fakeAppRunner.CreateAppArgsForCall(0)
   337  			Expect(createAppParams.Name).To(Equal("app-name"))
   338  			Expect(createAppParams.RootFS).To(Equal(droplet_runner.DropletRootFS))
   339  			Expect(createAppParams.StartCommand).To(Equal("/tmp/launcher"))
   340  			Expect(createAppParams.AppArgs).To(Equal([]string{"/home/vcap/app", "", "{}"}))
   341  			Expect(createAppParams.WorkingDir).To(Equal("/home/vcap"))
   342  			Expect(createAppParams.AppEnvironmentParams.EnvironmentVariables).To(matchers.ContainExactly(map[string]string{
   343  				"PWD":         "/home/vcap",
   344  				"TMPDIR":      "/home/vcap/tmp",
   345  				"http_proxy":  "",
   346  				"https_proxy": "",
   347  				"no_proxy":    "",
   348  			}))
   349  			Expect(createAppParams.Annotation).To(MatchJSON(`{
   350  				"droplet_source": {
   351  					"droplet_name": "droplet-name"
   352  				}
   353  			}`))
   354  
   355  			Expect(createAppParams.Setup).To(Equal(models.WrapAction(&models.SerialAction{
   356  				LogSource: "app-name",
   357  				Actions: []*models.Action{
   358  					models.WrapAction(&models.DownloadAction{
   359  						From: "http://file-server.service.cf.internal:8080/v1/static/cell-helpers/cell-helpers.tgz",
   360  						To:   "/tmp",
   361  						User: "vcap",
   362  					}),
   363  					models.WrapAction(&models.DownloadAction{
   364  						From: "http://dav-user:dav-pass@blob-host:7474/blobs/droplet-name-droplet.tgz",
   365  						To:   "/home/vcap",
   366  						User: "vcap",
   367  					}),
   368  				},
   369  			})))
   370  		})
   371  
   372  		It("launches the droplet lrp task with proxy environment variables", func() {
   373  			fakeBlobStore.DownloadReturns(ioutil.NopCloser(strings.NewReader("{}")), nil)
   374  			fakeBlobStore.DownloadDropletActionReturns(models.WrapAction(&models.DownloadAction{}))
   375  
   376  			fakeProxyConfReader.ProxyConfReturns(droplet_runner.ProxyConf{
   377  				HTTPProxy:  "http://proxy",
   378  				HTTPSProxy: "https://proxy",
   379  				NoProxy:    "no-proxy",
   380  			}, nil)
   381  
   382  			err := dropletRunner.LaunchDroplet("app-name", "droplet-name", "", []string{}, app_runner.AppEnvironmentParams{})
   383  			Expect(err).NotTo(HaveOccurred())
   384  
   385  			Expect(fakeAppRunner.CreateAppCallCount()).To(Equal(1))
   386  			createAppParams := fakeAppRunner.CreateAppArgsForCall(0)
   387  			Expect(createAppParams.AppEnvironmentParams.EnvironmentVariables).To(matchers.ContainExactly(map[string]string{
   388  				"PWD":         "/home/vcap",
   389  				"TMPDIR":      "/home/vcap/tmp",
   390  				"http_proxy":  "http://proxy",
   391  				"https_proxy": "https://proxy",
   392  				"no_proxy":    "no-proxy",
   393  			}))
   394  		})
   395  
   396  		It("launches the droplet lrp task with a custom start command", func() {
   397  			err := dropletRunner.LaunchDroplet("app-name", "droplet-name", "start-r-up", []string{"-yeah!"}, app_runner.AppEnvironmentParams{})
   398  			Expect(err).NotTo(HaveOccurred())
   399  
   400  			Expect(fakeAppRunner.CreateAppCallCount()).To(Equal(1))
   401  			createAppParams := fakeAppRunner.CreateAppArgsForCall(0)
   402  			Expect(createAppParams.Name).To(Equal("app-name"))
   403  			Expect(createAppParams.StartCommand).To(Equal("/tmp/launcher"))
   404  			Expect(createAppParams.AppArgs).To(Equal([]string{"/home/vcap/app", "start-r-up -yeah!", "{}"}))
   405  		})
   406  
   407  		It("returns an error when proxyConf reader fails", func() {
   408  			fakeBlobStore.DownloadReturns(ioutil.NopCloser(strings.NewReader("{}")), nil)
   409  			fakeBlobStore.DownloadDropletActionReturns(models.WrapAction(&models.DownloadAction{}))
   410  			fakeProxyConfReader.ProxyConfReturns(droplet_runner.ProxyConf{}, errors.New("proxyConf has failed"))
   411  
   412  			err := dropletRunner.LaunchDroplet("app-name", "droplet-name", "", []string{}, app_runner.AppEnvironmentParams{})
   413  			Expect(err).To(MatchError("proxyConf has failed"))
   414  		})
   415  
   416  		It("returns an error when create app fails", func() {
   417  			fakeBlobStore.DownloadReturns(ioutil.NopCloser(strings.NewReader(`{}`)), nil)
   418  			fakeAppRunner.CreateAppReturns(errors.New("nope"))
   419  
   420  			err := dropletRunner.LaunchDroplet("app-name", "droplet-name", "", []string{}, app_runner.AppEnvironmentParams{})
   421  			Expect(err).To(MatchError("nope"))
   422  		})
   423  	})
   424  
   425  	Describe("RemoveDroplet", func() {
   426  		It("recursively removes a droplets from the blob store", func() {
   427  			config.SetBlobStore("blob-host", "7474", "dav-user", "dav-pass")
   428  			Expect(config.Save()).To(Succeed())
   429  
   430  			fakeBlobStore.ListReturns([]blob.Blob{
   431  				{Path: "drippy-bits.zip"},
   432  				{Path: "drippy-droplet.tgz"},
   433  			}, nil)
   434  
   435  			appInfos := []app_examiner.AppInfo{
   436  				{
   437  					Annotation: "",
   438  				},
   439  				{
   440  					Annotation: "junk",
   441  				},
   442  				{
   443  					Annotation: `{
   444  						"droplet_source": {
   445  							"droplet_name": "other-drippy"
   446  						}
   447  					}`,
   448  				},
   449  			}
   450  			fakeAppExaminer.ListAppsReturns(appInfos, nil)
   451  
   452  			Expect(dropletRunner.RemoveDroplet("drippy")).To(Succeed())
   453  
   454  			Expect(fakeBlobStore.ListCallCount()).To(Equal(1))
   455  
   456  			Expect(fakeBlobStore.DeleteCallCount()).To(Equal(2))
   457  			Expect(fakeBlobStore.DeleteArgsForCall(0)).To(Equal("drippy-bits.zip"))
   458  			Expect(fakeBlobStore.DeleteArgsForCall(1)).To(Equal("drippy-droplet.tgz"))
   459  		})
   460  
   461  		It("returns an error when querying the blob store fails", func() {
   462  			fakeBlobStore.ListReturns(nil, errors.New("some error"))
   463  
   464  			err := dropletRunner.RemoveDroplet("drippy")
   465  			Expect(err).To(MatchError("some error"))
   466  		})
   467  
   468  		It("returns an error when the app specifies that the droplet is in use", func() {
   469  			config.SetBlobStore("blob-host", "7474", "dav-user", "dav-pass")
   470  			Expect(config.Save()).To(Succeed())
   471  
   472  			appInfos := []app_examiner.AppInfo{{
   473  				ProcessGuid: "dripapp",
   474  				Annotation: `{
   475  					"droplet_source": {
   476  						"droplet_name": "drippy"
   477  					}
   478  				}`,
   479  			}}
   480  			fakeAppExaminer.ListAppsReturns(appInfos, nil)
   481  
   482  			err := dropletRunner.RemoveDroplet("drippy")
   483  			Expect(err).To(MatchError("app dripapp was launched from droplet"))
   484  		})
   485  
   486  		It("returns an error when listing the running applications fails", func() {
   487  			fakeAppExaminer.ListAppsReturns(nil, errors.New("some error"))
   488  
   489  			err := dropletRunner.RemoveDroplet("drippy")
   490  			Expect(err).To(MatchError("some error"))
   491  		})
   492  
   493  		It("returns an error when the droplet doesn't exist", func() {
   494  			fakeBlobStore.ListReturns([]blob.Blob{
   495  				{Path: "drippy-bits.zip"},
   496  				{Path: "drippy-droplet.tgz"},
   497  			}, nil)
   498  
   499  			err := dropletRunner.RemoveDroplet("droopy")
   500  			Expect(err).To(MatchError("droplet not found"))
   501  		})
   502  	})
   503  
   504  	Describe("ExportDroplet", func() {
   505  		BeforeEach(func() {
   506  			fakeDropletReader := ioutil.NopCloser(strings.NewReader("some droplet reader"))
   507  
   508  			fakeBlobStore.DownloadStub = func(path string) (io.ReadCloser, error) {
   509  				switch path {
   510  				case "drippy-droplet.tgz":
   511  					return fakeDropletReader, nil
   512  				case "no-such-droplet-droplet.tgz":
   513  					return nil, errors.New("some missing droplet error")
   514  				default:
   515  					return nil, errors.New("fake GetReader called with invalid arguments")
   516  				}
   517  			}
   518  		})
   519  
   520  		It("returns IO readers for the droplet", func() {
   521  			dropletReader, err := dropletRunner.ExportDroplet("drippy")
   522  			defer dropletReader.Close()
   523  			Expect(err).NotTo(HaveOccurred())
   524  			Expect(ioutil.ReadAll(dropletReader)).To(BeEquivalentTo("some droplet reader"))
   525  		})
   526  
   527  		Context("when the droplet name does not have an associated droplet", func() {
   528  			It("returns an error", func() {
   529  				_, err := dropletRunner.ExportDroplet("no-such-droplet")
   530  				Expect(err).To(MatchError("droplet not found: some missing droplet error"))
   531  			})
   532  		})
   533  	})
   534  
   535  	Describe("ImportDroplet", func() {
   536  		var tmpDir, dropletPathArg string
   537  
   538  		BeforeEach(func() {
   539  			var err error
   540  			tmpDir, err = ioutil.TempDir(os.TempDir(), "droplet")
   541  			Expect(err).NotTo(HaveOccurred())
   542  
   543  			dropletPathArg = filepath.Join(tmpDir, "totally-drippy.tgz")
   544  			Expect(ioutil.WriteFile(dropletPathArg, []byte("droplet contents"), 0644)).To(Succeed())
   545  		})
   546  
   547  		AfterEach(func() {
   548  			Expect(os.RemoveAll(tmpDir)).To(Succeed())
   549  		})
   550  
   551  		Context("when the droplet files exist", func() {
   552  			It("uploads the droplet files to the blob store", func() {
   553  				err := dropletRunner.ImportDroplet("drippy", dropletPathArg)
   554  				Expect(err).NotTo(HaveOccurred())
   555  
   556  				Expect(fakeBlobStore.UploadCallCount()).To(Equal(1))
   557  
   558  				path, _ := fakeBlobStore.UploadArgsForCall(0)
   559  				Expect(path).To(Equal("drippy-droplet.tgz"))
   560  			})
   561  
   562  			Context("when the blob bucket returns error(s)", func() {
   563  				It("returns an error uploading the droplet file", func() {
   564  					fakeBlobStore.UploadReturns(errors.New("some error"))
   565  
   566  					err := dropletRunner.ImportDroplet("drippy", dropletPathArg)
   567  					Expect(err).To(MatchError("some error"))
   568  				})
   569  			})
   570  		})
   571  
   572  		Context("when the droplet files do not exist", func() {
   573  			It("returns an error opening the droplet file", func() {
   574  				err := dropletRunner.ImportDroplet("drippy", "some/missing/droplet/path")
   575  				Expect(reflect.TypeOf(err).String()).To(Equal("*os.PathError"))
   576  			})
   577  		})
   578  	})
   579  })