github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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/errors" 12 gitjujutesting "github.com/juju/testing" 13 jc "github.com/juju/testing/checkers" 14 gc "gopkg.in/check.v1" 15 "gopkg.in/juju/names.v2" 16 17 "github.com/juju/juju/api/base" 18 apicontroller "github.com/juju/juju/api/controller" 19 "github.com/juju/juju/apiserver/params" 20 "github.com/juju/juju/cmd/juju/controller" 21 "github.com/juju/juju/cmd/modelcmd" 22 cmdtesting "github.com/juju/juju/cmd/testing" 23 "github.com/juju/juju/environs" 24 "github.com/juju/juju/environs/config" 25 "github.com/juju/juju/jujuclient" 26 "github.com/juju/juju/jujuclient/jujuclienttesting" 27 "github.com/juju/juju/provider/dummy" 28 "github.com/juju/juju/testing" 29 ) 30 31 const ( 32 test1UUID = "1871299e-1370-4f3e-83ab-1849ed7b1076" 33 test2UUID = "c59d0e3b-2bd7-4867-b1b9-f1ef8a0bb004" 34 test3UUID = "82bf9738-764b-49c1-9c19-18f6ee155854" 35 36 test1ControllerUUID = "2371299e-1370-4f3e-83ab-1849ed7b1076" 37 test2ControllerUUID = "f89d0e3b-5bd7-9867-b1b9-f1ef8a0bb004" 38 test3ControllerUUID = "cfbf9738-764b-49c1-9c19-18f6ee155854" 39 ) 40 41 type DestroySuite struct { 42 baseDestroySuite 43 } 44 45 var _ = gc.Suite(&DestroySuite{}) 46 47 type baseDestroySuite struct { 48 testing.FakeJujuXDGDataHomeSuite 49 api *fakeDestroyAPI 50 clientapi *fakeDestroyAPIClient 51 store *jujuclienttesting.MemStore 52 apierror error 53 } 54 55 // fakeDestroyAPI mocks out the controller API 56 type fakeDestroyAPI struct { 57 gitjujutesting.Stub 58 cloud environs.CloudSpec 59 env map[string]interface{} 60 destroyAll bool 61 blocks []params.ModelBlockInfo 62 envStatus map[string]base.ModelStatus 63 allModels []base.UserModel 64 hostedConfig []apicontroller.HostedConfig 65 } 66 67 func (f *fakeDestroyAPI) Close() error { 68 f.MethodCall(f, "Close") 69 return f.NextErr() 70 } 71 72 func (f *fakeDestroyAPI) CloudSpec(tag names.ModelTag) (environs.CloudSpec, error) { 73 f.MethodCall(f, "CloudSpec", tag) 74 if err := f.NextErr(); err != nil { 75 return environs.CloudSpec{}, err 76 } 77 return f.cloud, nil 78 } 79 80 func (f *fakeDestroyAPI) ModelConfig() (map[string]interface{}, error) { 81 f.MethodCall(f, "ModelConfig") 82 if err := f.NextErr(); err != nil { 83 return nil, err 84 } 85 return f.env, nil 86 } 87 88 func (f *fakeDestroyAPI) HostedModelConfigs() ([]apicontroller.HostedConfig, error) { 89 f.MethodCall(f, "HostedModelConfigs") 90 if err := f.NextErr(); err != nil { 91 return nil, err 92 } 93 return f.hostedConfig, nil 94 } 95 96 func (f *fakeDestroyAPI) DestroyController(destroyAll bool) error { 97 f.MethodCall(f, "DestroyController", destroyAll) 98 f.destroyAll = destroyAll 99 return f.NextErr() 100 } 101 102 func (f *fakeDestroyAPI) ListBlockedModels() ([]params.ModelBlockInfo, error) { 103 f.MethodCall(f, "ListBlockedModels") 104 return f.blocks, f.NextErr() 105 } 106 107 func (f *fakeDestroyAPI) ModelStatus(tags ...names.ModelTag) ([]base.ModelStatus, error) { 108 f.MethodCall(f, "ModelStatus", tags) 109 status := make([]base.ModelStatus, len(tags)) 110 for i, tag := range tags { 111 status[i] = f.envStatus[tag.Id()] 112 } 113 return status, f.NextErr() 114 } 115 116 func (f *fakeDestroyAPI) AllModels() ([]base.UserModel, error) { 117 f.MethodCall(f, "AllModels") 118 return f.allModels, f.NextErr() 119 } 120 121 // fakeDestroyAPIClient mocks out the client API 122 type fakeDestroyAPIClient struct { 123 err error 124 modelgetcalled bool 125 destroycalled bool 126 } 127 128 func (f *fakeDestroyAPIClient) Close() error { return nil } 129 130 func (f *fakeDestroyAPIClient) ModelGet() (map[string]interface{}, error) { 131 f.modelgetcalled = true 132 if f.err != nil { 133 return nil, f.err 134 } 135 return map[string]interface{}{}, nil 136 } 137 138 func (f *fakeDestroyAPIClient) DestroyModel() error { 139 f.destroycalled = true 140 return f.err 141 } 142 143 func createBootstrapInfo(c *gc.C, name string) map[string]interface{} { 144 cfg, err := config.New(config.UseDefaults, map[string]interface{}{ 145 "type": "dummy", 146 "name": name, 147 "uuid": testing.ModelTag.Id(), 148 "controller": "true", 149 }) 150 c.Assert(err, jc.ErrorIsNil) 151 return cfg.AllAttrs() 152 } 153 154 func (s *baseDestroySuite) SetUpTest(c *gc.C) { 155 s.FakeJujuXDGDataHomeSuite.SetUpTest(c) 156 s.clientapi = &fakeDestroyAPIClient{} 157 owner := names.NewUserTag("owner") 158 s.api = &fakeDestroyAPI{ 159 cloud: dummy.SampleCloudSpec(), 160 envStatus: map[string]base.ModelStatus{}, 161 } 162 s.apierror = nil 163 164 s.store = jujuclienttesting.NewMemStore() 165 s.store.Controllers["test1"] = jujuclient.ControllerDetails{ 166 APIEndpoints: []string{"localhost"}, 167 CACert: testing.CACert, 168 ControllerUUID: test1ControllerUUID, 169 } 170 s.store.Controllers["test3"] = jujuclient.ControllerDetails{ 171 APIEndpoints: []string{"localhost"}, 172 CACert: testing.CACert, 173 ControllerUUID: test3ControllerUUID, 174 } 175 s.store.Accounts["test1"] = jujuclient.AccountDetails{ 176 User: "admin@local", 177 } 178 179 var modelList = []struct { 180 name string 181 controllerUUID string 182 modelUUID string 183 bootstrapCfg map[string]interface{} 184 }{ 185 { 186 name: "test1:admin", 187 controllerUUID: test1ControllerUUID, 188 modelUUID: test1UUID, 189 bootstrapCfg: createBootstrapInfo(c, "admin"), 190 }, { 191 name: "test2:test2", 192 controllerUUID: test2ControllerUUID, 193 modelUUID: test2UUID, 194 }, { 195 name: "test3:admin", 196 controllerUUID: test3ControllerUUID, 197 modelUUID: test3UUID, 198 }, 199 } 200 for _, model := range modelList { 201 controllerName, modelName := modelcmd.SplitModelName(model.name) 202 s.store.UpdateController(controllerName, jujuclient.ControllerDetails{ 203 ControllerUUID: model.controllerUUID, 204 APIEndpoints: []string{"localhost"}, 205 CACert: testing.CACert, 206 }) 207 s.store.UpdateModel(controllerName, modelName, jujuclient.ModelDetails{ 208 ModelUUID: model.modelUUID, 209 }) 210 if model.bootstrapCfg != nil { 211 s.store.BootstrapConfig[controllerName] = jujuclient.BootstrapConfig{ 212 ControllerModelUUID: model.modelUUID, 213 Config: createBootstrapInfo(c, "admin"), 214 CloudType: "dummy", 215 } 216 } 217 218 uuid := model.modelUUID 219 s.api.allModels = append(s.api.allModels, base.UserModel{ 220 Name: model.name, 221 UUID: uuid, 222 Owner: owner.Canonical(), 223 }) 224 s.api.envStatus[model.modelUUID] = base.ModelStatus{ 225 UUID: uuid, 226 Life: string(params.Dead), 227 HostedMachineCount: 0, 228 ServiceCount: 0, 229 Owner: owner.Canonical(), 230 } 231 } 232 } 233 234 func (s *DestroySuite) runDestroyCommand(c *gc.C, args ...string) (*cmd.Context, error) { 235 return testing.RunCommand(c, s.newDestroyCommand(), args...) 236 } 237 238 func (s *DestroySuite) newDestroyCommand() cmd.Command { 239 return controller.NewDestroyCommandForTest(s.api, s.clientapi, s.store, s.apierror) 240 } 241 242 func checkControllerExistsInStore(c *gc.C, name string, store jujuclient.ControllerGetter) { 243 _, err := store.ControllerByName(name) 244 c.Assert(err, jc.ErrorIsNil) 245 } 246 247 func checkControllerRemovedFromStore(c *gc.C, name string, store jujuclient.ControllerGetter) { 248 _, err := store.ControllerByName(name) 249 c.Assert(err, jc.Satisfies, errors.IsNotFound) 250 } 251 252 func (s *DestroySuite) TestDestroyNoControllerNameError(c *gc.C) { 253 _, err := s.runDestroyCommand(c) 254 c.Assert(err, gc.ErrorMatches, "no controller specified") 255 } 256 257 func (s *DestroySuite) TestDestroyBadFlags(c *gc.C) { 258 _, err := s.runDestroyCommand(c, "-n") 259 c.Assert(err, gc.ErrorMatches, "flag provided but not defined: -n") 260 } 261 262 func (s *DestroySuite) TestDestroyUnknownArgument(c *gc.C) { 263 _, err := s.runDestroyCommand(c, "model", "whoops") 264 c.Assert(err, gc.ErrorMatches, `unrecognized args: \["whoops"\]`) 265 } 266 267 func (s *DestroySuite) TestDestroyUnknownController(c *gc.C) { 268 _, err := s.runDestroyCommand(c, "foo") 269 c.Assert(err, gc.ErrorMatches, `controller foo not found`) 270 } 271 272 func (s *DestroySuite) TestDestroyControllerNotFoundNotRemovedFromStore(c *gc.C) { 273 s.apierror = errors.NotFoundf("test1") 274 _, err := s.runDestroyCommand(c, "test1", "-y") 275 c.Assert(err, gc.ErrorMatches, "cannot connect to API: test1 not found") 276 c.Check(c.GetTestLog(), jc.Contains, "If the controller is unusable") 277 checkControllerExistsInStore(c, "test1", s.store) 278 } 279 280 func (s *DestroySuite) TestDestroyCannotConnectToAPI(c *gc.C) { 281 s.apierror = errors.New("connection refused") 282 _, err := s.runDestroyCommand(c, "test1", "-y") 283 c.Assert(err, gc.ErrorMatches, "cannot connect to API: connection refused") 284 c.Check(c.GetTestLog(), jc.Contains, "If the controller is unusable") 285 checkControllerExistsInStore(c, "test1", s.store) 286 } 287 288 func (s *DestroySuite) TestDestroy(c *gc.C) { 289 _, err := s.runDestroyCommand(c, "test1", "-y") 290 c.Assert(err, jc.ErrorIsNil) 291 c.Assert(s.api.destroyAll, jc.IsFalse) 292 c.Assert(s.clientapi.destroycalled, jc.IsFalse) 293 checkControllerRemovedFromStore(c, "test1", s.store) 294 } 295 296 func (s *DestroySuite) TestDestroyAlias(c *gc.C) { 297 _, err := s.runDestroyCommand(c, "test1", "-y") 298 c.Assert(err, jc.ErrorIsNil) 299 c.Assert(s.api.destroyAll, jc.IsFalse) 300 c.Assert(s.clientapi.destroycalled, jc.IsFalse) 301 checkControllerRemovedFromStore(c, "test1", s.store) 302 } 303 304 func (s *DestroySuite) TestDestroyWithDestroyAllModelsFlag(c *gc.C) { 305 _, err := s.runDestroyCommand(c, "test1", "-y", "--destroy-all-models") 306 c.Assert(err, jc.ErrorIsNil) 307 c.Assert(s.api.destroyAll, jc.IsTrue) 308 checkControllerRemovedFromStore(c, "test1", s.store) 309 } 310 311 func (s *DestroySuite) TestDestroyControllerGetFails(c *gc.C) { 312 s.api.SetErrors(errors.NotFoundf(`controller "test3"`)) 313 _, err := s.runDestroyCommand(c, "test3", "-y") 314 c.Assert(err, gc.ErrorMatches, 315 "getting controller environ: getting model config from API: controller \"test3\" not found", 316 ) 317 checkControllerExistsInStore(c, "test3", s.store) 318 } 319 320 func (s *DestroySuite) TestFailedDestroyController(c *gc.C) { 321 s.api.SetErrors(errors.New("permission denied")) 322 _, err := s.runDestroyCommand(c, "test1", "-y") 323 c.Assert(err, gc.ErrorMatches, "cannot destroy controller: permission denied") 324 c.Assert(s.api.destroyAll, jc.IsFalse) 325 checkControllerExistsInStore(c, "test1", s.store) 326 } 327 328 func (s *DestroySuite) TestDestroyControllerAliveModels(c *gc.C) { 329 for uuid, status := range s.api.envStatus { 330 status.Life = string(params.Alive) 331 s.api.envStatus[uuid] = status 332 } 333 s.api.SetErrors(¶ms.Error{Code: params.CodeHasHostedModels}) 334 _, err := s.runDestroyCommand(c, "test1", "-y") 335 c.Assert(err.Error(), gc.Equals, `cannot destroy controller "test1" 336 337 The controller has live hosted models. If you want 338 to destroy all hosted models in the controller, 339 run this command again with the --destroy-all-models 340 flag. 341 342 Models: 343 owner@local/test2:test2 (alive) 344 owner@local/test3:admin (alive) 345 `) 346 347 } 348 349 func (s *DestroySuite) TestDestroyControllerReattempt(c *gc.C) { 350 // The first attempt to destroy should yield an error 351 // saying that the controller has hosted models. After 352 // checking, we find there are only dead hosted models, 353 // and reattempt the destroy the controller; this time 354 // it succeeds. 355 s.api.SetErrors(¶ms.Error{Code: params.CodeHasHostedModels}) 356 _, err := s.runDestroyCommand(c, "test1", "-y") 357 c.Assert(err, jc.ErrorIsNil) 358 s.api.CheckCallNames(c, 359 "DestroyController", 360 "AllModels", 361 "ModelStatus", 362 "ModelStatus", 363 "DestroyController", 364 "AllModels", 365 "ModelStatus", 366 "ModelStatus", 367 "Close", 368 ) 369 } 370 371 func (s *DestroySuite) resetController(c *gc.C) { 372 s.store.Controllers["test1"] = jujuclient.ControllerDetails{ 373 APIEndpoints: []string{"localhost"}, 374 CACert: testing.CACert, 375 ControllerUUID: test1UUID, 376 } 377 s.store.Accounts["test1"] = jujuclient.AccountDetails{ 378 User: "admin@local", 379 } 380 s.store.BootstrapConfig["test1"] = jujuclient.BootstrapConfig{ 381 ControllerModelUUID: test1UUID, 382 Config: createBootstrapInfo(c, "admin"), 383 CloudType: "dummy", 384 } 385 } 386 387 func (s *DestroySuite) TestDestroyCommandConfirmation(c *gc.C) { 388 var stdin, stdout bytes.Buffer 389 ctx := testing.Context(c) 390 ctx.Stdout = &stdout 391 ctx.Stdin = &stdin 392 393 // Ensure confirmation is requested if "-y" is not specified. 394 stdin.WriteString("n") 395 _, errc := cmdtesting.RunCommand(ctx, s.newDestroyCommand(), "test1") 396 select { 397 case err := <-errc: 398 c.Check(err, gc.ErrorMatches, "controller destruction aborted") 399 case <-time.After(testing.LongWait): 400 c.Fatalf("command took too long") 401 } 402 c.Check(testing.Stdout(ctx), gc.Matches, "WARNING!.*test1(.|\n)*") 403 checkControllerExistsInStore(c, "test1", s.store) 404 405 // EOF on stdin: equivalent to answering no. 406 stdin.Reset() 407 stdout.Reset() 408 _, errc = cmdtesting.RunCommand(ctx, s.newDestroyCommand(), "test1") 409 select { 410 case err := <-errc: 411 c.Check(err, gc.ErrorMatches, "controller destruction aborted") 412 case <-time.After(testing.LongWait): 413 c.Fatalf("command took too long") 414 } 415 c.Check(testing.Stdout(ctx), gc.Matches, "WARNING!.*test1(.|\n)*") 416 checkControllerExistsInStore(c, "test1", s.store) 417 418 for _, answer := range []string{"y", "Y", "yes", "YES"} { 419 stdin.Reset() 420 stdout.Reset() 421 stdin.WriteString(answer) 422 _, errc = cmdtesting.RunCommand(ctx, s.newDestroyCommand(), "test1") 423 select { 424 case err := <-errc: 425 c.Check(err, jc.ErrorIsNil) 426 case <-time.After(testing.LongWait): 427 c.Fatalf("command took too long") 428 } 429 checkControllerRemovedFromStore(c, "test1", s.store) 430 431 // Add the test1 controller back into the store for the next test 432 s.resetController(c) 433 } 434 } 435 436 func (s *DestroySuite) TestBlockedDestroy(c *gc.C) { 437 s.api.SetErrors(¶ms.Error{Code: params.CodeOperationBlocked}) 438 s.runDestroyCommand(c, "test1", "-y") 439 testLog := c.GetTestLog() 440 c.Check(testLog, jc.Contains, "To enable controller destruction, please run:") 441 c.Check(testLog, jc.Contains, "juju enable-destroy-controller") 442 } 443 444 func (s *DestroySuite) TestDestroyListBlocksError(c *gc.C) { 445 s.api.SetErrors( 446 ¶ms.Error{Code: params.CodeOperationBlocked}, 447 errors.New("unexpected api error"), 448 ) 449 s.runDestroyCommand(c, "test1", "-y") 450 testLog := c.GetTestLog() 451 c.Check(testLog, jc.Contains, "To enable controller destruction, please run:") 452 c.Check(testLog, jc.Contains, "juju enable-destroy-controller") 453 c.Check(testLog, jc.Contains, "Unable to list models: unexpected api error") 454 } 455 456 func (s *DestroySuite) TestDestroyReturnsBlocks(c *gc.C) { 457 s.api.SetErrors(¶ms.Error{Code: params.CodeOperationBlocked}) 458 s.api.blocks = []params.ModelBlockInfo{ 459 params.ModelBlockInfo{ 460 Name: "test1", 461 UUID: test1UUID, 462 OwnerTag: "user-cheryl@local", 463 Blocks: []string{ 464 "BlockDestroy", 465 }, 466 }, 467 params.ModelBlockInfo{ 468 Name: "test2", 469 UUID: test2UUID, 470 OwnerTag: "user-bob@local", 471 Blocks: []string{ 472 "BlockDestroy", 473 "BlockChange", 474 }, 475 }, 476 } 477 ctx, _ := s.runDestroyCommand(c, "test1", "-y", "--destroy-all-models") 478 c.Assert(testing.Stderr(ctx), gc.Equals, "Destroying controller\n"+ 479 "NAME MODEL UUID OWNER DISABLED COMMANDS\n"+ 480 "test1 1871299e-1370-4f3e-83ab-1849ed7b1076 cheryl@local destroy-model\n"+ 481 "test2 c59d0e3b-2bd7-4867-b1b9-f1ef8a0bb004 bob@local all, destroy-model\n") 482 c.Assert(testing.Stdout(ctx), gc.Equals, "") 483 }