github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/controller/destroy_test.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package controller_test 5 6 import ( 7 "bytes" 8 "time" 9 10 "github.com/juju/cmd" 11 "github.com/juju/cmd/cmdtesting" 12 "github.com/juju/errors" 13 gitjujutesting "github.com/juju/testing" 14 jc "github.com/juju/testing/checkers" 15 gc "gopkg.in/check.v1" 16 "gopkg.in/juju/names.v2" 17 18 "github.com/juju/juju/api/base" 19 apicontroller "github.com/juju/juju/api/controller" 20 "github.com/juju/juju/apiserver/params" 21 "github.com/juju/juju/cmd/cmdtest" 22 "github.com/juju/juju/cmd/juju/controller" 23 "github.com/juju/juju/cmd/modelcmd" 24 "github.com/juju/juju/environs" 25 "github.com/juju/juju/environs/config" 26 "github.com/juju/juju/environs/context" 27 "github.com/juju/juju/jujuclient" 28 "github.com/juju/juju/provider/dummy" 29 "github.com/juju/juju/testing" 30 ) 31 32 const ( 33 test1UUID = "1871299e-1370-4f3e-83ab-1849ed7b1076" 34 test2UUID = "c59d0e3b-2bd7-4867-b1b9-f1ef8a0bb004" 35 test3UUID = "82bf9738-764b-49c1-9c19-18f6ee155854" 36 37 test1ControllerUUID = "2371299e-1370-4f3e-83ab-1849ed7b1076" 38 test2ControllerUUID = "f89d0e3b-5bd7-9867-b1b9-f1ef8a0bb004" 39 test3ControllerUUID = "cfbf9738-764b-49c1-9c19-18f6ee155854" 40 ) 41 42 type DestroySuite struct { 43 baseDestroySuite 44 } 45 46 var _ = gc.Suite(&DestroySuite{}) 47 48 type baseDestroySuite struct { 49 testing.FakeJujuXDGDataHomeSuite 50 api *fakeDestroyAPI 51 clientapi *fakeDestroyAPIClient 52 storageAPI *mockStorageAPI 53 store *jujuclient.MemStore 54 apierror error 55 56 controllerCredentialAPI *mockCredentialAPI 57 environsDestroy func(string, environs.ControllerDestroyer, context.ProviderCallContext, jujuclient.ControllerStore) error 58 } 59 60 // fakeDestroyAPI mocks out the controller API 61 type fakeDestroyAPI struct { 62 gitjujutesting.Stub 63 cloud environs.CloudSpec 64 env map[string]interface{} 65 blocks []params.ModelBlockInfo 66 envStatus map[string]base.ModelStatus 67 allModels []base.UserModel 68 hostedConfig []apicontroller.HostedConfig 69 bestAPIVersion int 70 } 71 72 func (f *fakeDestroyAPI) BestAPIVersion() int { 73 return f.bestAPIVersion 74 } 75 76 func (f *fakeDestroyAPI) Close() error { 77 f.MethodCall(f, "Close") 78 return f.NextErr() 79 } 80 81 func (f *fakeDestroyAPI) CloudSpec(tag names.ModelTag) (environs.CloudSpec, error) { 82 f.MethodCall(f, "CloudSpec", tag) 83 if err := f.NextErr(); err != nil { 84 return environs.CloudSpec{}, err 85 } 86 return f.cloud, nil 87 } 88 89 func (f *fakeDestroyAPI) ModelConfig() (map[string]interface{}, error) { 90 f.MethodCall(f, "ModelConfig") 91 if err := f.NextErr(); err != nil { 92 return nil, err 93 } 94 return f.env, nil 95 } 96 97 func (f *fakeDestroyAPI) HostedModelConfigs() ([]apicontroller.HostedConfig, error) { 98 f.MethodCall(f, "HostedModelConfigs") 99 if err := f.NextErr(); err != nil { 100 return nil, err 101 } 102 return f.hostedConfig, nil 103 } 104 105 func (f *fakeDestroyAPI) DestroyController(args apicontroller.DestroyControllerParams) error { 106 f.MethodCall(f, "DestroyController", args) 107 return f.NextErr() 108 } 109 110 func (f *fakeDestroyAPI) ListBlockedModels() ([]params.ModelBlockInfo, error) { 111 f.MethodCall(f, "ListBlockedModels") 112 return f.blocks, f.NextErr() 113 } 114 115 func (f *fakeDestroyAPI) ModelStatus(tags ...names.ModelTag) ([]base.ModelStatus, error) { 116 f.MethodCall(f, "ModelStatus", tags) 117 status := make([]base.ModelStatus, len(tags)) 118 for i, tag := range tags { 119 status[i] = f.envStatus[tag.Id()] 120 } 121 return status, f.NextErr() 122 } 123 124 func (f *fakeDestroyAPI) AllModels() ([]base.UserModel, error) { 125 f.MethodCall(f, "AllModels") 126 return f.allModels, f.NextErr() 127 } 128 129 // fakeDestroyAPIClient mocks out the client API 130 type fakeDestroyAPIClient struct { 131 err error 132 modelgetcalled bool 133 destroycalled bool 134 } 135 136 func (f *fakeDestroyAPIClient) Close() error { return nil } 137 138 func (f *fakeDestroyAPIClient) ModelGet() (map[string]interface{}, error) { 139 f.modelgetcalled = true 140 if f.err != nil { 141 return nil, f.err 142 } 143 return map[string]interface{}{}, nil 144 } 145 146 func (f *fakeDestroyAPIClient) DestroyModel() error { 147 f.destroycalled = true 148 return f.err 149 } 150 151 func createBootstrapInfo(c *gc.C, name string) map[string]interface{} { 152 cfg, err := config.New(config.UseDefaults, map[string]interface{}{ 153 "type": "dummy", 154 "name": name, 155 "uuid": testing.ModelTag.Id(), 156 "controller": "true", 157 }) 158 c.Assert(err, jc.ErrorIsNil) 159 return cfg.AllAttrs() 160 } 161 162 func (s *baseDestroySuite) SetUpTest(c *gc.C) { 163 s.FakeJujuXDGDataHomeSuite.SetUpTest(c) 164 s.clientapi = &fakeDestroyAPIClient{} 165 owner := names.NewUserTag("owner") 166 s.api = &fakeDestroyAPI{ 167 cloud: dummy.SampleCloudSpec(), 168 envStatus: map[string]base.ModelStatus{}, 169 bestAPIVersion: 4, 170 } 171 s.apierror = nil 172 173 s.storageAPI = &mockStorageAPI{} 174 s.controllerCredentialAPI = &mockCredentialAPI{} 175 s.environsDestroy = environs.Destroy 176 177 s.store = jujuclient.NewMemStore() 178 s.store.Controllers["test1"] = jujuclient.ControllerDetails{ 179 APIEndpoints: []string{"localhost"}, 180 CACert: testing.CACert, 181 ControllerUUID: test1ControllerUUID, 182 } 183 s.store.Controllers["test3"] = jujuclient.ControllerDetails{ 184 APIEndpoints: []string{"localhost"}, 185 CACert: testing.CACert, 186 ControllerUUID: test3ControllerUUID, 187 } 188 s.store.Accounts["test1"] = jujuclient.AccountDetails{ 189 User: "admin", 190 } 191 192 var modelList = []struct { 193 name string 194 controllerUUID string 195 modelUUID string 196 bootstrapCfg map[string]interface{} 197 }{ 198 { 199 name: "test1:admin", 200 controllerUUID: test1ControllerUUID, 201 modelUUID: test1UUID, 202 bootstrapCfg: createBootstrapInfo(c, "admin"), 203 }, { 204 name: "test2:test2", 205 controllerUUID: test2ControllerUUID, 206 modelUUID: test2UUID, 207 }, { 208 name: "test3:admin", 209 controllerUUID: test3ControllerUUID, 210 modelUUID: test3UUID, 211 }, 212 } 213 for _, model := range modelList { 214 controllerName, modelName := modelcmd.SplitModelName(model.name) 215 s.store.UpdateController(controllerName, jujuclient.ControllerDetails{ 216 ControllerUUID: model.controllerUUID, 217 APIEndpoints: []string{"localhost"}, 218 CACert: testing.CACert, 219 }) 220 s.store.UpdateModel(controllerName, modelName, jujuclient.ModelDetails{ 221 ModelUUID: model.modelUUID, 222 }) 223 if model.bootstrapCfg != nil { 224 s.store.BootstrapConfig[controllerName] = jujuclient.BootstrapConfig{ 225 ControllerModelUUID: model.modelUUID, 226 Config: createBootstrapInfo(c, "admin"), 227 CloudType: "dummy", 228 } 229 } 230 231 uuid := model.modelUUID 232 s.api.allModels = append(s.api.allModels, base.UserModel{ 233 Name: model.name, 234 UUID: uuid, 235 Owner: owner.Id(), 236 }) 237 s.api.envStatus[model.modelUUID] = base.ModelStatus{ 238 UUID: uuid, 239 Life: string(params.Dead), 240 HostedMachineCount: 0, 241 ApplicationCount: 0, 242 Owner: owner.Id(), 243 } 244 } 245 } 246 247 func (s *DestroySuite) runDestroyCommand(c *gc.C, args ...string) (*cmd.Context, error) { 248 return cmdtesting.RunCommand(c, s.newDestroyCommand(), args...) 249 } 250 251 func (s *DestroySuite) newDestroyCommand() cmd.Command { 252 return controller.NewDestroyCommandForTest( 253 s.api, s.clientapi, s.storageAPI, s.store, s.apierror, 254 func() (controller.CredentialAPI, error) { return s.controllerCredentialAPI, nil }, 255 s.environsDestroy, 256 ) 257 } 258 259 func checkControllerExistsInStore(c *gc.C, name string, store jujuclient.ControllerGetter) { 260 _, err := store.ControllerByName(name) 261 c.Assert(err, jc.ErrorIsNil) 262 } 263 264 func checkControllerRemovedFromStore(c *gc.C, name string, store jujuclient.ControllerGetter) { 265 _, err := store.ControllerByName(name) 266 c.Assert(err, jc.Satisfies, errors.IsNotFound) 267 } 268 269 func (s *DestroySuite) TestDestroyNoControllerNameError(c *gc.C) { 270 _, err := s.runDestroyCommand(c) 271 c.Assert(err, gc.ErrorMatches, "no controller specified") 272 } 273 274 func (s *DestroySuite) TestDestroyBadFlags(c *gc.C) { 275 _, err := s.runDestroyCommand(c, "-n") 276 c.Assert(err, gc.ErrorMatches, "option provided but not defined: -n") 277 } 278 279 func (s *DestroySuite) TestDestroyUnknownArgument(c *gc.C) { 280 _, err := s.runDestroyCommand(c, "model", "whoops") 281 c.Assert(err, gc.ErrorMatches, `unrecognized args: \["whoops"\]`) 282 } 283 284 func (s *DestroySuite) TestDestroyUnknownController(c *gc.C) { 285 _, err := s.runDestroyCommand(c, "foo") 286 c.Assert(err, gc.ErrorMatches, `controller foo not found`) 287 } 288 289 func (s *DestroySuite) TestDestroyControllerNotFoundNotRemovedFromStore(c *gc.C) { 290 s.apierror = errors.NotFoundf("test1") 291 _, err := s.runDestroyCommand(c, "test1", "-y") 292 c.Assert(err, gc.ErrorMatches, "cannot connect to API: test1 not found") 293 c.Check(c.GetTestLog(), jc.Contains, "If the controller is unusable") 294 checkControllerExistsInStore(c, "test1", s.store) 295 } 296 297 func (s *DestroySuite) TestDestroyCannotConnectToAPI(c *gc.C) { 298 s.apierror = errors.New("connection refused") 299 _, err := s.runDestroyCommand(c, "test1", "-y") 300 c.Assert(err, gc.ErrorMatches, "cannot connect to API: connection refused") 301 c.Check(c.GetTestLog(), jc.Contains, "If the controller is unusable") 302 checkControllerExistsInStore(c, "test1", s.store) 303 } 304 305 func (s *DestroySuite) TestDestroy(c *gc.C) { 306 _, err := s.runDestroyCommand(c, "test1", "-y") 307 c.Assert(err, jc.ErrorIsNil) 308 c.Assert(s.clientapi.destroycalled, jc.IsFalse) 309 checkControllerRemovedFromStore(c, "test1", s.store) 310 } 311 312 func (s *DestroySuite) TestDestroyAlias(c *gc.C) { 313 _, err := s.runDestroyCommand(c, "test1", "-y") 314 c.Assert(err, jc.ErrorIsNil) 315 c.Assert(s.clientapi.destroycalled, jc.IsFalse) 316 checkControllerRemovedFromStore(c, "test1", s.store) 317 } 318 319 func (s *DestroySuite) TestDestroyWithDestroyAllModelsFlag(c *gc.C) { 320 _, err := s.runDestroyCommand(c, "test1", "-y", "--destroy-all-models") 321 c.Assert(err, jc.ErrorIsNil) 322 s.api.CheckCallNames(c, "DestroyController", "AllModels", "ModelStatus", "Close") 323 s.api.CheckCall(c, 0, "DestroyController", apicontroller.DestroyControllerParams{ 324 DestroyModels: true, 325 }) 326 checkControllerRemovedFromStore(c, "test1", s.store) 327 } 328 329 func (s *DestroySuite) TestDestroyWithDestroyDestroyStorageFlag(c *gc.C) { 330 _, err := s.runDestroyCommand(c, "test1", "-y", "--destroy-storage") 331 c.Assert(err, jc.ErrorIsNil) 332 destroyStorage := true 333 s.api.CheckCall(c, 0, "DestroyController", apicontroller.DestroyControllerParams{ 334 DestroyStorage: &destroyStorage, 335 }) 336 } 337 338 func (s *DestroySuite) TestDestroyWithDestroyReleaseStorageFlag(c *gc.C) { 339 _, err := s.runDestroyCommand(c, "test1", "-y", "--release-storage") 340 c.Assert(err, jc.ErrorIsNil) 341 destroyStorage := false 342 s.api.CheckCall(c, 0, "DestroyController", apicontroller.DestroyControllerParams{ 343 DestroyStorage: &destroyStorage, 344 }) 345 } 346 347 func (s *DestroySuite) TestDestroyWithDestroyDestroyReleaseStorageFlagsMutuallyExclusive(c *gc.C) { 348 _, err := s.runDestroyCommand(c, "test1", "-y", "--destroy-storage", "--release-storage") 349 c.Assert(err, gc.ErrorMatches, "--destroy-storage and --release-storage cannot both be specified") 350 } 351 352 func (s *DestroySuite) TestDestroyWithDestroyDestroyStorageFlagUnspecified(c *gc.C) { 353 var haveFilesystem bool 354 for uuid, status := range s.api.envStatus { 355 status.Life = string(params.Alive) 356 status.Volumes = append(status.Volumes, base.Volume{Detachable: true}) 357 if !haveFilesystem { 358 haveFilesystem = true 359 status.Filesystems = append( 360 status.Filesystems, base.Filesystem{Detachable: true}, 361 ) 362 } 363 s.api.envStatus[uuid] = status 364 } 365 366 s.api.SetErrors(¶ms.Error{Code: params.CodeHasPersistentStorage}) 367 _, err := s.runDestroyCommand(c, "test1", "-y", "--destroy-all-models") 368 c.Assert(err.Error(), gc.Equals, `cannot destroy controller "test1" 369 370 The controller has persistent storage remaining: 371 3 volumes and 1 filesystem across 3 models 372 373 To destroy the storage, run the destroy-controller 374 command again with the "--destroy-storage" option. 375 376 To release the storage from Juju's management 377 without destroying it, use the "--release-storage" 378 option instead. The storage can then be imported 379 into another Juju model. 380 381 `) 382 } 383 384 func (s *DestroySuite) TestDestroyWithDestroyDestroyStorageFlagUnspecifiedOldController(c *gc.C) { 385 s.api.bestAPIVersion = 3 386 s.storageAPI.storage = []params.StorageDetails{{}} 387 388 _, err := s.runDestroyCommand(c, "test1", "-y") 389 c.Assert(err, gc.ErrorMatches, `cannot destroy controller "test1" 390 391 Destroying this controller will destroy the storage, 392 but you have not indicated that you want to do that. 393 394 Please run the the command again with --destroy-storage 395 to confirm that you want to destroy the storage along 396 with the controller. 397 398 If instead you want to keep the storage, you must first 399 upgrade the controller to version 2.3 or greater. 400 401 `) 402 } 403 404 func (s *DestroySuite) TestDestroyWithDestroyDestroyStorageFlagUnspecifiedOldControllerNoStorage(c *gc.C) { 405 s.api.bestAPIVersion = 3 406 s.storageAPI.storage = nil // no storage 407 408 _, err := s.runDestroyCommand(c, "test1", "-y") 409 c.Assert(err, jc.ErrorIsNil) 410 } 411 412 func (s *DestroySuite) TestDestroyControllerGetFails(c *gc.C) { 413 s.api.SetErrors(errors.NotFoundf(`controller "test3"`)) 414 _, err := s.runDestroyCommand(c, "test3", "-y") 415 c.Assert(err, gc.ErrorMatches, 416 "getting controller environ: getting model config from API: controller \"test3\" not found", 417 ) 418 checkControllerExistsInStore(c, "test3", s.store) 419 } 420 421 func (s *DestroySuite) TestFailedDestroyController(c *gc.C) { 422 s.api.SetErrors(errors.New("permission denied")) 423 _, err := s.runDestroyCommand(c, "test1", "-y") 424 c.Assert(err, gc.ErrorMatches, "cannot destroy controller: permission denied") 425 checkControllerExistsInStore(c, "test1", s.store) 426 } 427 428 func (s *DestroySuite) TestDestroyControllerAliveModels(c *gc.C) { 429 for uuid, status := range s.api.envStatus { 430 status.Life = string(params.Alive) 431 s.api.envStatus[uuid] = status 432 } 433 s.api.SetErrors(¶ms.Error{Code: params.CodeHasHostedModels}) 434 _, err := s.runDestroyCommand(c, "test1", "-y") 435 c.Assert(err.Error(), gc.Equals, `cannot destroy controller "test1" 436 437 The controller has live hosted models. If you want 438 to destroy all hosted models in the controller, 439 run this command again with the --destroy-all-models 440 option. 441 442 Models: 443 owner/test2:test2 (alive) 444 owner/test3:admin (alive) 445 `) 446 447 } 448 449 func (s *DestroySuite) TestDestroyControllerReattempt(c *gc.C) { 450 // The first attempt to destroy should yield an error 451 // saying that the controller has hosted models. After 452 // checking, we find there are only dead hosted models, 453 // and reattempt the destroy the controller; this time 454 // it succeeds. 455 s.api.SetErrors(¶ms.Error{Code: params.CodeHasHostedModels}) 456 _, err := s.runDestroyCommand(c, "test1", "-y") 457 c.Assert(err, jc.ErrorIsNil) 458 s.api.CheckCallNames(c, 459 "DestroyController", 460 "AllModels", 461 "ModelStatus", 462 "DestroyController", 463 "AllModels", 464 "ModelStatus", 465 "Close", 466 ) 467 } 468 469 func (s *DestroySuite) resetController(c *gc.C) { 470 s.store.Controllers["test1"] = jujuclient.ControllerDetails{ 471 APIEndpoints: []string{"localhost"}, 472 CACert: testing.CACert, 473 ControllerUUID: test1UUID, 474 } 475 s.store.Accounts["test1"] = jujuclient.AccountDetails{ 476 User: "admin", 477 } 478 s.store.BootstrapConfig["test1"] = jujuclient.BootstrapConfig{ 479 ControllerModelUUID: test1UUID, 480 Config: createBootstrapInfo(c, "admin"), 481 CloudType: "dummy", 482 } 483 } 484 485 func (s *DestroySuite) TestDestroyCommandConfirmation(c *gc.C) { 486 var stdin, stdout bytes.Buffer 487 ctx := cmdtesting.Context(c) 488 ctx.Stdout = &stdout 489 ctx.Stdin = &stdin 490 491 // Ensure confirmation is requested if "-y" is not specified. 492 stdin.WriteString("n") 493 _, errc := cmdtest.RunCommandWithDummyProvider(ctx, s.newDestroyCommand(), "test1") 494 select { 495 case err := <-errc: 496 c.Check(err, gc.ErrorMatches, "controller destruction aborted") 497 case <-time.After(testing.LongWait): 498 c.Fatalf("command took too long") 499 } 500 c.Check(cmdtesting.Stdout(ctx), gc.Matches, "WARNING!.*test1(.|\n)*") 501 checkControllerExistsInStore(c, "test1", s.store) 502 503 // EOF on stdin: equivalent to answering no. 504 stdin.Reset() 505 stdout.Reset() 506 _, errc = cmdtest.RunCommandWithDummyProvider(ctx, s.newDestroyCommand(), "test1") 507 select { 508 case err := <-errc: 509 c.Check(err, gc.ErrorMatches, "controller destruction aborted") 510 case <-time.After(testing.LongWait): 511 c.Fatalf("command took too long") 512 } 513 c.Check(cmdtesting.Stdout(ctx), gc.Matches, "WARNING!.*test1(.|\n)*") 514 checkControllerExistsInStore(c, "test1", s.store) 515 516 for _, answer := range []string{"y", "Y", "yes", "YES"} { 517 stdin.Reset() 518 stdout.Reset() 519 stdin.WriteString(answer) 520 _, errc = cmdtest.RunCommandWithDummyProvider(ctx, s.newDestroyCommand(), "test1") 521 select { 522 case err := <-errc: 523 c.Check(err, jc.ErrorIsNil) 524 case <-time.After(testing.LongWait): 525 c.Fatalf("command took too long") 526 } 527 checkControllerRemovedFromStore(c, "test1", s.store) 528 529 // Add the test1 controller back into the store for the next test 530 s.resetController(c) 531 } 532 } 533 534 func (s *DestroySuite) TestBlockedDestroy(c *gc.C) { 535 s.api.SetErrors(¶ms.Error{Code: params.CodeOperationBlocked}) 536 s.runDestroyCommand(c, "test1", "-y") 537 testLog := c.GetTestLog() 538 c.Check(testLog, jc.Contains, "To enable controller destruction, please run:") 539 c.Check(testLog, jc.Contains, "juju enable-destroy-controller") 540 } 541 542 func (s *DestroySuite) TestDestroyListBlocksError(c *gc.C) { 543 s.api.SetErrors( 544 ¶ms.Error{Code: params.CodeOperationBlocked}, 545 errors.New("unexpected api error"), 546 ) 547 s.runDestroyCommand(c, "test1", "-y") 548 testLog := c.GetTestLog() 549 c.Check(testLog, jc.Contains, "To enable controller destruction, please run:") 550 c.Check(testLog, jc.Contains, "juju enable-destroy-controller") 551 c.Check(testLog, jc.Contains, "Unable to list models: unexpected api error") 552 } 553 554 func (s *DestroySuite) TestDestroyReturnsBlocks(c *gc.C) { 555 s.api.SetErrors(¶ms.Error{Code: params.CodeOperationBlocked}) 556 s.api.blocks = []params.ModelBlockInfo{ 557 { 558 Name: "test1", 559 UUID: test1UUID, 560 OwnerTag: "user-cheryl", 561 Blocks: []string{ 562 "BlockDestroy", 563 }, 564 }, 565 { 566 Name: "test2", 567 UUID: test2UUID, 568 OwnerTag: "user-bob", 569 Blocks: []string{ 570 "BlockDestroy", 571 "BlockChange", 572 }, 573 }, 574 } 575 ctx, _ := s.runDestroyCommand(c, "test1", "-y", "--destroy-all-models") 576 c.Assert(cmdtesting.Stderr(ctx), gc.Equals, "Destroying controller\n"+ 577 "Name Model UUID Owner Disabled commands\n"+ 578 "test1 1871299e-1370-4f3e-83ab-1849ed7b1076 cheryl destroy-model\n"+ 579 "test2 c59d0e3b-2bd7-4867-b1b9-f1ef8a0bb004 bob all, destroy-model\n") 580 c.Assert(cmdtesting.Stdout(ctx), gc.Equals, "") 581 } 582 583 func (s *DestroySuite) TestDestroyWithInvalidCredentialCallbackExecutingSuccessfully(c *gc.C) { 584 s.destroyAndInvalidateCredential(c) 585 } 586 587 func (s *DestroySuite) destroyAndInvalidateCredential(c *gc.C) { 588 s.destroyAndInvalidateCredentialWithError(c, "") 589 } 590 591 func (s *DestroySuite) destroyAndInvalidateCredentialWithError(c *gc.C, expectedErr string) { 592 called := false 593 // Make sure that the invalidate credential callback in the cloud context 594 // is called. 595 s.environsDestroy = func(controllerName string, 596 env environs.ControllerDestroyer, 597 ctx context.ProviderCallContext, 598 store jujuclient.ControllerStore, 599 ) error { 600 called = true 601 err := ctx.InvalidateCredential("testing now") 602 if expectedErr == "" { 603 c.Assert(err, jc.ErrorIsNil) 604 } else { 605 c.Assert(err, gc.ErrorMatches, expectedErr) 606 } 607 return environs.Destroy(controllerName, env, ctx, store) 608 } 609 _, err := s.runDestroyCommand(c, "test1", "-y") 610 c.Assert(err, jc.ErrorIsNil) 611 c.Assert(called, jc.IsTrue) 612 s.controllerCredentialAPI.CheckCallNames(c, "InvalidateModelCredential", "Close") 613 } 614 615 func (s *DestroySuite) TestDestroyWithInvalidCredentialCallbackFailing(c *gc.C) { 616 msg := "unexpected creds callback error" 617 s.controllerCredentialAPI.SetErrors(errors.New(msg)) 618 // As we are throwing the error on within the callback, 619 // the actual call to destroy should succeed. 620 s.destroyAndInvalidateCredentialWithError(c, msg) 621 } 622 623 func (s *DestroySuite) TestDestroyWithInvalidCredentialCallbackFailingToCloseAPI(c *gc.C) { 624 s.controllerCredentialAPI.SetErrors( 625 nil, // call to invalidate credential succeeds 626 errors.New("unexpected creds callback error"), // call to close api client fails 627 ) 628 // As we are throwing the error on api.Close for callback, 629 // the actual call to destroy should succeed. 630 s.destroyAndInvalidateCredential(c) 631 } 632 633 type mockStorageAPI struct { 634 gitjujutesting.Stub 635 storage []params.StorageDetails 636 } 637 638 func (m *mockStorageAPI) Close() error { 639 m.MethodCall(m, "Close") 640 return m.NextErr() 641 } 642 643 func (m *mockStorageAPI) ListStorageDetails() ([]params.StorageDetails, error) { 644 m.MethodCall(m, "ListStorageDetails") 645 return m.storage, m.NextErr() 646 } 647 648 type mockCredentialAPI struct { 649 gitjujutesting.Stub 650 } 651 652 func (m *mockCredentialAPI) InvalidateModelCredential(reason string) error { 653 m.MethodCall(m, "InvalidateModelCredential", reason) 654 return m.NextErr() 655 } 656 657 func (m *mockCredentialAPI) Close() error { 658 m.MethodCall(m, "Close") 659 return m.NextErr() 660 }