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