1package deployment_test
2
3import (
4	. "github.com/cloudfoundry/bosh-cli/deployment"
5
6	"time"
7
8	. "github.com/onsi/ginkgo"
9	. "github.com/onsi/gomega"
10
11	mock_agentclient "github.com/cloudfoundry/bosh-cli/agentclient/mocks"
12	mock_blobstore "github.com/cloudfoundry/bosh-cli/blobstore/mocks"
13	mock_cloud "github.com/cloudfoundry/bosh-cli/cloud/mocks"
14	mock_instance_state "github.com/cloudfoundry/bosh-cli/deployment/instance/state/mocks"
15	"github.com/golang/mock/gomock"
16
17	bias "github.com/cloudfoundry/bosh-agent/agentclient/applyspec"
18	bicloud "github.com/cloudfoundry/bosh-cli/cloud"
19	biconfig "github.com/cloudfoundry/bosh-cli/config"
20	bidisk "github.com/cloudfoundry/bosh-cli/deployment/disk"
21	biinstance "github.com/cloudfoundry/bosh-cli/deployment/instance"
22	bisshtunnel "github.com/cloudfoundry/bosh-cli/deployment/sshtunnel"
23	bivm "github.com/cloudfoundry/bosh-cli/deployment/vm"
24	bistemcell "github.com/cloudfoundry/bosh-cli/stemcell"
25	bosherr "github.com/cloudfoundry/bosh-utils/errors"
26	boshlog "github.com/cloudfoundry/bosh-utils/logger"
27	boshsys "github.com/cloudfoundry/bosh-utils/system"
28	fakesys "github.com/cloudfoundry/bosh-utils/system/fakes"
29	fakeuuid "github.com/cloudfoundry/bosh-utils/uuid/fakes"
30
31	fakebiui "github.com/cloudfoundry/bosh-cli/ui/fakes"
32)
33
34var _ = Describe("Deployment", func() {
35	var mockCtrl *gomock.Controller
36
37	BeforeEach(func() {
38		mockCtrl = gomock.NewController(GinkgoT())
39	})
40
41	AfterEach(func() {
42		mockCtrl.Finish()
43	})
44
45	Describe("Delete", func() {
46		var (
47			logger boshlog.Logger
48			fs     boshsys.FileSystem
49
50			fakeUUIDGenerator      *fakeuuid.FakeGenerator
51			fakeRepoUUIDGenerator  *fakeuuid.FakeGenerator
52			deploymentStateService biconfig.DeploymentStateService
53			vmRepo                 biconfig.VMRepo
54			diskRepo               biconfig.DiskRepo
55			stemcellRepo           biconfig.StemcellRepo
56
57			mockCloud       *mock_cloud.MockCloud
58			mockAgentClient *mock_agentclient.MockAgentClient
59
60			mockStateBuilderFactory *mock_instance_state.MockBuilderFactory
61			mockStateBuilder        *mock_instance_state.MockBuilder
62			mockState               *mock_instance_state.MockState
63
64			mockBlobstore *mock_blobstore.MockBlobstore
65
66			fakeStage *fakebiui.FakeStage
67
68			deploymentFactory Factory
69
70			stemcellApiVersion = 2
71			deployment         Deployment
72			skipDrain          bool
73		)
74
75		var expectNormalFlow = func() {
76			gomock.InOrder(
77				mockCloud.EXPECT().HasVM("fake-vm-cid").Return(true, nil),
78				mockAgentClient.EXPECT().Ping().Return("any-state", nil),                   // ping to make sure agent is responsive
79				mockAgentClient.EXPECT().Drain("shutdown"),                                 // drain all jobs
80				mockAgentClient.EXPECT().Stop(),                                            // stop all jobs
81				mockAgentClient.EXPECT().ListDisk().Return([]string{"fake-disk-cid"}, nil), // get mounted disks to be unmounted
82				mockAgentClient.EXPECT().UnmountDisk("fake-disk-cid"),
83				mockCloud.EXPECT().DeleteVM("fake-vm-cid"),
84				mockCloud.EXPECT().DeleteDisk("fake-disk-cid"),
85				mockCloud.EXPECT().DeleteStemcell("fake-stemcell-cid"),
86			)
87		}
88
89		var expectDrainlessFlow = func() {
90			gomock.InOrder(
91				mockCloud.EXPECT().HasVM("fake-vm-cid").Return(true, nil),
92				mockAgentClient.EXPECT().Ping().Return("any-state", nil),                   // ping to make sure agent is responsive
93				mockAgentClient.EXPECT().Stop(),                                            // stop all jobs
94				mockAgentClient.EXPECT().ListDisk().Return([]string{"fake-disk-cid"}, nil), // get mounted disks to be unmounted
95				mockAgentClient.EXPECT().UnmountDisk("fake-disk-cid"),
96				mockCloud.EXPECT().DeleteVM("fake-vm-cid"),
97				mockCloud.EXPECT().DeleteDisk("fake-disk-cid"),
98				mockCloud.EXPECT().DeleteStemcell("fake-stemcell-cid"),
99			)
100		}
101		var allowApplySpecToBeCreated = func() {
102			jobName := "fake-job-name"
103			jobIndex := 0
104
105			applySpec := bias.ApplySpec{
106				Deployment: "test-release",
107				Index:      jobIndex,
108				Packages:   map[string]bias.Blob{},
109				Networks: map[string]interface{}{
110					"network-1": map[string]interface{}{
111						"cloud_properties": map[string]interface{}{},
112						"type":             "dynamic",
113						"ip":               "",
114					},
115				},
116				Job: bias.Job{
117					Name:      jobName,
118					Templates: []bias.Blob{},
119				},
120				RenderedTemplatesArchive: bias.RenderedTemplatesArchiveSpec{},
121				ConfigurationHash:        "",
122			}
123
124			mockStateBuilderFactory.EXPECT().NewBuilder(mockBlobstore, mockAgentClient).Return(mockStateBuilder).AnyTimes()
125			mockState.EXPECT().ToApplySpec().Return(applySpec).AnyTimes()
126		}
127
128		BeforeEach(func() {
129			logger = boshlog.NewLogger(boshlog.LevelNone)
130			fs = fakesys.NewFakeFileSystem()
131
132			fakeUUIDGenerator = fakeuuid.NewFakeGenerator()
133			deploymentStateService = biconfig.NewFileSystemDeploymentStateService(fs, fakeUUIDGenerator, logger, "/deployment.json")
134
135			fakeRepoUUIDGenerator = fakeuuid.NewFakeGenerator()
136			vmRepo = biconfig.NewVMRepo(deploymentStateService)
137			diskRepo = biconfig.NewDiskRepo(deploymentStateService, fakeRepoUUIDGenerator)
138			stemcellRepo = biconfig.NewStemcellRepo(deploymentStateService, fakeRepoUUIDGenerator)
139
140			mockCloud = mock_cloud.NewMockCloud(mockCtrl)
141			mockAgentClient = mock_agentclient.NewMockAgentClient(mockCtrl)
142
143			fakeStage = fakebiui.NewFakeStage()
144
145			pingTimeout := 10 * time.Second
146			pingDelay := 500 * time.Millisecond
147			deploymentFactory = NewFactory(pingTimeout, pingDelay)
148
149			skipDrain = false
150		})
151
152		JustBeforeEach(func() {
153			// all these local factories & managers are just used to construct a Deployment based on the deployment state
154			diskManagerFactory := bidisk.NewManagerFactory(diskRepo, logger)
155			diskDeployer := bivm.NewDiskDeployer(diskManagerFactory, diskRepo, logger, false)
156
157			vmManagerFactory := bivm.NewManagerFactory(vmRepo, stemcellRepo, diskDeployer, fakeUUIDGenerator, fs, logger)
158			sshTunnelFactory := bisshtunnel.NewFactory(logger)
159
160			mockStateBuilderFactory = mock_instance_state.NewMockBuilderFactory(mockCtrl)
161			mockStateBuilder = mock_instance_state.NewMockBuilder(mockCtrl)
162			mockState = mock_instance_state.NewMockState(mockCtrl)
163
164			instanceFactory := biinstance.NewFactory(mockStateBuilderFactory)
165			instanceManagerFactory := biinstance.NewManagerFactory(sshTunnelFactory, instanceFactory, logger)
166			stemcellManagerFactory := bistemcell.NewManagerFactory(stemcellRepo)
167
168			mockBlobstore = mock_blobstore.NewMockBlobstore(mockCtrl)
169
170			deploymentManagerFactory := NewManagerFactory(vmManagerFactory, instanceManagerFactory, diskManagerFactory, stemcellManagerFactory, deploymentFactory)
171			deploymentManager := deploymentManagerFactory.NewManager(mockCloud, mockAgentClient, mockBlobstore)
172
173			allowApplySpecToBeCreated()
174
175			var err error
176			deployment, _, err = deploymentManager.FindCurrent()
177			Expect(err).ToNot(HaveOccurred())
178			//Note: deployment will be nil if the config has no vms, disks, or stemcells
179		})
180
181		Context("when the deployment has been deployed", func() {
182			BeforeEach(func() {
183				// create deployment manifest yaml file
184				deploymentStateService.Save(biconfig.DeploymentState{
185					DirectorID:        "fake-director-id",
186					InstallationID:    "fake-installation-id",
187					CurrentVMCID:      "fake-vm-cid",
188					CurrentStemcellID: "fake-stemcell-guid",
189					CurrentDiskID:     "fake-disk-guid",
190					Disks: []biconfig.DiskRecord{
191						{
192							ID:   "fake-disk-guid",
193							CID:  "fake-disk-cid",
194							Size: 100,
195						},
196					},
197					Stemcells: []biconfig.StemcellRecord{
198						{
199							ID:  "fake-stemcell-guid",
200							CID: "fake-stemcell-cid",
201						},
202					},
203				})
204			})
205
206			It("stops agent, unmounts disk, deletes vm, deletes disk, deletes stemcell", func() {
207				expectNormalFlow()
208
209				err := deployment.Delete(skipDrain, fakeStage)
210				Expect(err).ToNot(HaveOccurred())
211			})
212
213			It("skips draining if specified", func() {
214				skipDrain = true
215				expectDrainlessFlow()
216
217				err := deployment.Delete(skipDrain, fakeStage)
218				Expect(err).ToNot(HaveOccurred())
219			})
220
221			It("logs validation stages", func() {
222				expectNormalFlow()
223
224				err := deployment.Delete(skipDrain, fakeStage)
225				Expect(err).ToNot(HaveOccurred())
226
227				Expect(fakeStage.PerformCalls).To(Equal([]*fakebiui.PerformCall{
228					{Name: "Waiting for the agent on VM 'fake-vm-cid'"},
229					{Name: "Draining jobs on instance 'unknown/0'"},
230					{Name: "Stopping jobs on instance 'unknown/0'"},
231					{Name: "Unmounting disk 'fake-disk-cid'"},
232					{Name: "Deleting VM 'fake-vm-cid'"},
233					{Name: "Deleting disk 'fake-disk-cid'"},
234					{Name: "Deleting stemcell 'fake-stemcell-cid'"},
235				}))
236			})
237
238			It("clears current vm, disk and stemcell", func() {
239				expectNormalFlow()
240
241				err := deployment.Delete(skipDrain, fakeStage)
242				Expect(err).ToNot(HaveOccurred())
243
244				_, found, err := vmRepo.FindCurrent()
245				Expect(found).To(BeFalse(), "should be no current VM")
246
247				_, found, err = diskRepo.FindCurrent()
248				Expect(found).To(BeFalse(), "should be no current disk")
249
250				diskRecords, err := diskRepo.All()
251				Expect(err).ToNot(HaveOccurred())
252				Expect(diskRecords).To(BeEmpty(), "expected no disk records")
253
254				_, found, err = stemcellRepo.FindCurrent()
255				Expect(found).To(BeFalse(), "should be no current stemcell")
256
257				stemcellRecords, err := stemcellRepo.All()
258				Expect(err).ToNot(HaveOccurred())
259				Expect(stemcellRecords).To(BeEmpty(), "expected no stemcell records")
260			})
261
262			//TODO: It'd be nice to test recovering after agent was responsive, before timeout (hard to do with gomock)
263			Context("when agent is unresponsive", func() {
264				BeforeEach(func() {
265					// reduce timout & delay to reduce test duration
266					pingTimeout := 1 * time.Second
267					pingDelay := 100 * time.Millisecond
268					deploymentFactory = NewFactory(pingTimeout, pingDelay)
269				})
270
271				It("times out pinging agent, deletes vm, deletes disk, deletes stemcell", func() {
272					gomock.InOrder(
273						mockCloud.EXPECT().HasVM("fake-vm-cid").Return(true, nil),
274						mockAgentClient.EXPECT().Ping().Return("", bosherr.Error("unresponsive agent")).AnyTimes(), // ping to make sure agent is responsive
275						mockCloud.EXPECT().DeleteVM("fake-vm-cid"),
276						mockCloud.EXPECT().DeleteDisk("fake-disk-cid"),
277						mockCloud.EXPECT().DeleteStemcell("fake-stemcell-cid"),
278					)
279
280					err := deployment.Delete(skipDrain, fakeStage)
281					Expect(err).ToNot(HaveOccurred())
282				})
283			})
284
285			Context("and delete previously suceeded", func() {
286				JustBeforeEach(func() {
287					expectNormalFlow()
288
289					err := deployment.Delete(skipDrain, fakeStage)
290					Expect(err).ToNot(HaveOccurred())
291
292					// reset event log recording
293					fakeStage = fakebiui.NewFakeStage()
294				})
295
296				It("does not delete anything", func() {
297					err := deployment.Delete(skipDrain, fakeStage)
298					Expect(err).ToNot(HaveOccurred())
299
300					Expect(fakeStage.PerformCalls).To(BeEmpty())
301				})
302			})
303		})
304
305		Context("when nothing has been deployed", func() {
306			BeforeEach(func() {
307				deploymentStateService.Save(biconfig.DeploymentState{})
308			})
309
310			JustBeforeEach(func() {
311				// A previous JustBeforeEach uses FindCurrent to define deployment,
312				// which would return a nil if the config is empty.
313				// So we have to make a fake empty deployment to test it.
314				deployment = deploymentFactory.NewDeployment([]biinstance.Instance{}, []bidisk.Disk{}, []bistemcell.CloudStemcell{})
315			})
316
317			It("does not delete anything", func() {
318				err := deployment.Delete(skipDrain, fakeStage)
319				Expect(err).NotTo(HaveOccurred())
320
321				Expect(fakeStage.PerformCalls).To(BeEmpty())
322			})
323		})
324
325		Context("when VM has been deployed", func() {
326			var (
327				expectHasVM *gomock.Call
328			)
329			BeforeEach(func() {
330				deploymentStateService.Save(biconfig.DeploymentState{})
331				vmRepo.UpdateCurrent("fake-vm-cid")
332
333				expectHasVM = mockCloud.EXPECT().HasVM("fake-vm-cid").Return(true, nil)
334			})
335
336			It("stops the agent and deletes the VM", func() {
337				gomock.InOrder(
338					mockAgentClient.EXPECT().Ping().Return("any-state", nil),                   // ping to make sure agent is responsive
339					mockAgentClient.EXPECT().Drain("shutdown"),                                 // drain all jobs
340					mockAgentClient.EXPECT().Stop(),                                            // stop all jobs
341					mockAgentClient.EXPECT().ListDisk().Return([]string{"fake-disk-cid"}, nil), // get mounted disks to be unmounted
342					mockAgentClient.EXPECT().UnmountDisk("fake-disk-cid"),
343					mockCloud.EXPECT().DeleteVM("fake-vm-cid"),
344				)
345
346				err := deployment.Delete(skipDrain, fakeStage)
347				Expect(err).ToNot(HaveOccurred())
348			})
349
350			Context("when VM has been deleted manually (outside of bosh)", func() {
351				BeforeEach(func() {
352					expectHasVM.Return(false, nil)
353				})
354
355				It("skips agent shutdown & deletes the VM (to ensure related resources are released by the CPI)", func() {
356					mockCloud.EXPECT().DeleteVM("fake-vm-cid")
357
358					err := deployment.Delete(skipDrain, fakeStage)
359					Expect(err).ToNot(HaveOccurred())
360				})
361
362				It("ignores VMNotFound errors", func() {
363					mockCloud.EXPECT().DeleteVM("fake-vm-cid").Return(bicloud.NewCPIError("delete_vm", bicloud.CmdError{
364						Type:    bicloud.VMNotFoundError,
365						Message: "fake-vm-not-found-message",
366					}))
367
368					err := deployment.Delete(skipDrain, fakeStage)
369					Expect(err).ToNot(HaveOccurred())
370				})
371			})
372		})
373
374		Context("when a current disk exists", func() {
375			BeforeEach(func() {
376				deploymentStateService.Save(biconfig.DeploymentState{})
377				diskRecord, err := diskRepo.Save("fake-disk-cid", 100, nil)
378				Expect(err).ToNot(HaveOccurred())
379				diskRepo.UpdateCurrent(diskRecord.ID)
380			})
381
382			It("deletes the disk", func() {
383				mockCloud.EXPECT().DeleteDisk("fake-disk-cid")
384
385				err := deployment.Delete(skipDrain, fakeStage)
386				Expect(err).ToNot(HaveOccurred())
387			})
388
389			Context("when current disk has been deleted manually (outside of bosh)", func() {
390				It("deletes the disk (to ensure related resources are released by the CPI)", func() {
391					mockCloud.EXPECT().DeleteDisk("fake-disk-cid")
392
393					err := deployment.Delete(skipDrain, fakeStage)
394					Expect(err).ToNot(HaveOccurred())
395				})
396
397				It("ignores DiskNotFound errors", func() {
398					mockCloud.EXPECT().DeleteDisk("fake-disk-cid").Return(bicloud.NewCPIError("delete_disk", bicloud.CmdError{
399						Type:    bicloud.DiskNotFoundError,
400						Message: "fake-disk-not-found-message",
401					}))
402
403					err := deployment.Delete(skipDrain, fakeStage)
404					Expect(err).ToNot(HaveOccurred())
405				})
406			})
407		})
408
409		Context("when a current stemcell exists", func() {
410			BeforeEach(func() {
411				deploymentStateService.Save(biconfig.DeploymentState{})
412				stemcellRecord, err := stemcellRepo.Save("fake-stemcell-name", "fake-stemcell-version", "fake-stemcell-cid", stemcellApiVersion)
413				Expect(err).ToNot(HaveOccurred())
414				stemcellRepo.UpdateCurrent(stemcellRecord.ID)
415			})
416
417			It("deletes the stemcell", func() {
418				mockCloud.EXPECT().DeleteStemcell("fake-stemcell-cid")
419
420				err := deployment.Delete(skipDrain, fakeStage)
421				Expect(err).ToNot(HaveOccurred())
422			})
423
424			Context("when current stemcell has been deleted manually (outside of bosh)", func() {
425				It("deletes the stemcell (to ensure related resources are released by the CPI)", func() {
426					mockCloud.EXPECT().DeleteStemcell("fake-stemcell-cid")
427
428					err := deployment.Delete(skipDrain, fakeStage)
429					Expect(err).ToNot(HaveOccurred())
430				})
431
432				It("ignores StemcellNotFound errors", func() {
433					mockCloud.EXPECT().DeleteStemcell("fake-stemcell-cid").Return(bicloud.NewCPIError("delete_stemcell", bicloud.CmdError{
434						Type:    bicloud.StemcellNotFoundError,
435						Message: "fake-stemcell-not-found-message",
436					}))
437
438					err := deployment.Delete(skipDrain, fakeStage)
439					Expect(err).ToNot(HaveOccurred())
440				})
441			})
442		})
443	})
444})
445