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