github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/model/destroy_test.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package model_test 5 6 import ( 7 "bytes" 8 "time" 9 10 testclock "github.com/juju/clock/testclock" 11 "github.com/juju/cmd" 12 "github.com/juju/cmd/cmdtesting" 13 "github.com/juju/errors" 14 jutesting "github.com/juju/testing" 15 jc "github.com/juju/testing/checkers" 16 gc "gopkg.in/check.v1" 17 "gopkg.in/juju/names.v2" 18 "gopkg.in/macaroon-bakery.v2-unstable/httpbakery" 19 20 "github.com/juju/juju/api/base" 21 "github.com/juju/juju/apiserver/common" 22 "github.com/juju/juju/apiserver/params" 23 "github.com/juju/juju/cmd/cmdtest" 24 "github.com/juju/juju/cmd/juju/model" 25 rcmd "github.com/juju/juju/cmd/juju/romulus" 26 "github.com/juju/juju/cmd/modelcmd" 27 coremodel "github.com/juju/juju/core/model" 28 "github.com/juju/juju/jujuclient" 29 _ "github.com/juju/juju/provider/dummy" 30 "github.com/juju/juju/testing" 31 ) 32 33 type DestroySuite struct { 34 testing.FakeJujuXDGDataHomeSuite 35 api *fakeAPI 36 configAPI *fakeConfigAPI 37 storageAPI *mockStorageAPI 38 stub *jutesting.Stub 39 budgetAPIClient *mockBudgetAPIClient 40 store *jujuclient.MemStore 41 42 clock *testclock.Clock 43 } 44 45 var _ = gc.Suite(&DestroySuite{}) 46 47 // fakeDestroyAPI mocks out the client API 48 type fakeAPI struct { 49 *jutesting.Stub 50 err error 51 env map[string]interface{} 52 statusCallCount int 53 bestAPIVersion int 54 modelInfoErr []*params.Error 55 modelStatusPayload []base.ModelStatus 56 } 57 58 func (f *fakeAPI) Close() error { return nil } 59 60 func (f *fakeAPI) BestAPIVersion() int { 61 return f.bestAPIVersion 62 } 63 64 func (f *fakeAPI) DestroyModel(tag names.ModelTag, destroyStorage *bool) error { 65 f.MethodCall(f, "DestroyModel", tag, destroyStorage) 66 return f.NextErr() 67 } 68 69 func (f *fakeAPI) ModelStatus(models ...names.ModelTag) ([]base.ModelStatus, error) { 70 var err error 71 if f.statusCallCount < len(f.modelInfoErr) { 72 modelInfoErr := f.modelInfoErr[f.statusCallCount] 73 if modelInfoErr != nil { 74 err = modelInfoErr 75 } 76 } else { 77 err = ¶ms.Error{Code: params.CodeNotFound} 78 } 79 f.statusCallCount++ 80 81 if f.modelStatusPayload == nil { 82 f.modelStatusPayload = []base.ModelStatus{{ 83 Volumes: []base.Volume{ 84 {Detachable: true}, 85 {Detachable: true}, 86 }, 87 Filesystems: []base.Filesystem{{Detachable: true}}, 88 }} 89 } 90 return f.modelStatusPayload, err 91 } 92 93 // fakeConfigAPI mocks out the ModelConfigAPI. 94 type fakeConfigAPI struct { 95 err error 96 slaLevel string 97 } 98 99 func (f *fakeConfigAPI) SLALevel() (string, error) { 100 return f.slaLevel, f.err 101 } 102 103 func (f *fakeConfigAPI) Close() error { return nil } 104 105 func (s *DestroySuite) SetUpTest(c *gc.C) { 106 s.FakeJujuXDGDataHomeSuite.SetUpTest(c) 107 s.stub = &jutesting.Stub{} 108 s.api = &fakeAPI{ 109 Stub: s.stub, 110 bestAPIVersion: 4, 111 } 112 s.configAPI = &fakeConfigAPI{} 113 s.storageAPI = &mockStorageAPI{Stub: s.stub} 114 s.clock = testclock.NewClock(time.Now()) 115 116 s.store = jujuclient.NewMemStore() 117 s.store.CurrentControllerName = "test1" 118 s.store.Controllers["test1"] = jujuclient.ControllerDetails{ControllerUUID: "test1-uuid"} 119 s.store.Models["test1"] = &jujuclient.ControllerModels{ 120 Models: map[string]jujuclient.ModelDetails{ 121 "admin/test1": {ModelUUID: "test1-uuid", ModelType: coremodel.IAAS}, 122 "admin/test2": {ModelUUID: "test2-uuid", ModelType: coremodel.IAAS}, 123 }, 124 } 125 s.store.Accounts["test1"] = jujuclient.AccountDetails{ 126 User: "admin", 127 } 128 129 s.budgetAPIClient = &mockBudgetAPIClient{Stub: s.stub} 130 s.PatchValue(model.GetBudgetAPIClient, 131 func(string, *httpbakery.Client) (model.BudgetAPIClient, error) { return s.budgetAPIClient, nil }) 132 s.PatchValue(&rcmd.GetMeteringURLForModelCmd, 133 func(*modelcmd.ModelCommandBase) (string, error) { return "http://example.com", nil }) 134 } 135 136 func (s *DestroySuite) runDestroyCommand(c *gc.C, args ...string) (*cmd.Context, error) { 137 cmd := model.NewDestroyCommandForTest(s.api, s.configAPI, s.storageAPI, s.clock, noOpRefresh, s.store) 138 return cmdtesting.RunCommand(c, cmd, args...) 139 } 140 141 func (s *DestroySuite) NewDestroyCommand() cmd.Command { 142 return model.NewDestroyCommandForTest(s.api, s.configAPI, s.storageAPI, s.clock, noOpRefresh, s.store) 143 } 144 145 func checkModelExistsInStore(c *gc.C, name string, store jujuclient.ClientStore) { 146 controller, model := modelcmd.SplitModelName(name) 147 _, err := store.ModelByName(controller, model) 148 c.Assert(err, jc.ErrorIsNil) 149 } 150 151 func checkModelRemovedFromStore(c *gc.C, name string, store jujuclient.ClientStore) { 152 controller, model := modelcmd.SplitModelName(name) 153 _, err := store.ModelByName(controller, model) 154 c.Assert(err, jc.Satisfies, errors.IsNotFound) 155 } 156 157 func (s *DestroySuite) TestDestroyNoModelNameError(c *gc.C) { 158 _, err := s.runDestroyCommand(c) 159 c.Assert(err, gc.ErrorMatches, "no model specified") 160 } 161 162 func (s *DestroySuite) TestDestroyBadFlags(c *gc.C) { 163 _, err := s.runDestroyCommand(c, "-n") 164 c.Assert(err, gc.ErrorMatches, "option provided but not defined: -n") 165 } 166 167 func (s *DestroySuite) TestDestroyUnknownArgument(c *gc.C) { 168 _, err := s.runDestroyCommand(c, "model", "whoops") 169 c.Assert(err, gc.ErrorMatches, `unrecognized args: \["whoops"\]`) 170 } 171 172 func (s *DestroySuite) TestDestroyUnknownModelCallsRefresh(c *gc.C) { 173 called := false 174 refresh := func(jujuclient.ClientStore, string) error { 175 called = true 176 return nil 177 } 178 179 cmd := model.NewDestroyCommandForTest(s.api, s.configAPI, s.storageAPI, s.clock, refresh, s.store) 180 _, err := cmdtesting.RunCommand(c, cmd, "foo") 181 c.Check(called, jc.IsTrue) 182 c.Check(err, gc.ErrorMatches, `model test1:admin/foo not found`) 183 } 184 185 func (s *DestroySuite) TestDestroyCannotConnectToAPI(c *gc.C) { 186 s.stub.SetErrors(errors.New("connection refused")) 187 _, err := s.runDestroyCommand(c, "test2", "-y") 188 c.Assert(err, gc.ErrorMatches, "cannot destroy model: connection refused") 189 c.Check(c.GetTestLog(), jc.Contains, "failed to destroy model \"test2\"") 190 checkModelExistsInStore(c, "test1:admin/test2", s.store) 191 } 192 193 func (s *DestroySuite) TestSystemDestroyFails(c *gc.C) { 194 _, err := s.runDestroyCommand(c, "test1", "-y") 195 c.Assert(err, gc.ErrorMatches, `"test1" is a controller; use 'juju destroy-controller' to destroy it`) 196 checkModelExistsInStore(c, "test1:admin/test1", s.store) 197 } 198 199 func (s *DestroySuite) TestDestroy(c *gc.C) { 200 checkModelExistsInStore(c, "test1:admin/test2", s.store) 201 _, err := s.runDestroyCommand(c, "test2", "-y") 202 c.Assert(err, jc.ErrorIsNil) 203 checkModelRemovedFromStore(c, "test1:admin/test2", s.store) 204 s.stub.CheckCalls(c, []jutesting.StubCall{ 205 {"DestroyModel", []interface{}{names.NewModelTag("test2-uuid"), (*bool)(nil)}}, 206 }) 207 } 208 209 func (s *DestroySuite) TestDestroyBlocks(c *gc.C) { 210 checkModelExistsInStore(c, "test1:admin/test2", s.store) 211 s.api.modelInfoErr = []*params.Error{{}, {Code: params.CodeNotFound}} 212 _, err := s.runDestroyCommand(c, "test2", "-y") 213 c.Assert(err, jc.ErrorIsNil) 214 checkModelRemovedFromStore(c, "test1:admin/test2", s.store) 215 c.Assert(s.api.statusCallCount, gc.Equals, 1) 216 } 217 218 func (s *DestroySuite) TestFailedDestroyModel(c *gc.C) { 219 s.stub.SetErrors(errors.New("permission denied")) 220 _, err := s.runDestroyCommand(c, "test1:test2", "-y") 221 c.Assert(err, gc.ErrorMatches, "cannot destroy model: permission denied") 222 checkModelExistsInStore(c, "test1:admin/test2", s.store) 223 } 224 225 func (s *DestroySuite) TestDestroyWithUnsupportedSLA(c *gc.C) { 226 s.configAPI.slaLevel = "unsupported" 227 _, err := s.runDestroyCommand(c, "test1:test2", "-y") 228 c.Assert(err, jc.ErrorIsNil) 229 s.stub.CheckCallNames(c, "DestroyModel") 230 } 231 232 func (s *DestroySuite) TestDestroyWithSupportedSLA(c *gc.C) { 233 s.configAPI.slaLevel = "standard" 234 _, err := s.runDestroyCommand(c, "test2", "-y") 235 c.Assert(err, jc.ErrorIsNil) 236 s.stub.CheckCalls(c, []jutesting.StubCall{ 237 {"DestroyModel", []interface{}{names.NewModelTag("test2-uuid"), (*bool)(nil)}}, 238 {"DeleteBudget", []interface{}{"test2-uuid"}}, 239 }) 240 } 241 242 func (s *DestroySuite) TestDestroyWithSupportedSLAFailure(c *gc.C) { 243 s.configAPI.slaLevel = "standard" 244 s.stub.SetErrors(nil, errors.New("bah")) 245 _, err := s.runDestroyCommand(c, "test2", "-y") 246 c.Assert(err, jc.ErrorIsNil) 247 s.stub.CheckCalls(c, []jutesting.StubCall{ 248 {"DestroyModel", []interface{}{names.NewModelTag("test2-uuid"), (*bool)(nil)}}, 249 {"DeleteBudget", []interface{}{"test2-uuid"}}, 250 }) 251 } 252 253 func (s *DestroySuite) TestDestroyDestroyStorage(c *gc.C) { 254 _, err := s.runDestroyCommand(c, "test2", "-y", "--destroy-storage") 255 c.Assert(err, jc.ErrorIsNil) 256 destroyStorage := true 257 s.stub.CheckCalls(c, []jutesting.StubCall{ 258 {"DestroyModel", []interface{}{names.NewModelTag("test2-uuid"), &destroyStorage}}, 259 }) 260 } 261 262 func (s *DestroySuite) TestDestroyReleaseStorage(c *gc.C) { 263 _, err := s.runDestroyCommand(c, "test2", "-y", "--release-storage") 264 c.Assert(err, jc.ErrorIsNil) 265 destroyStorage := false 266 s.stub.CheckCalls(c, []jutesting.StubCall{ 267 {"DestroyModel", []interface{}{names.NewModelTag("test2-uuid"), &destroyStorage}}, 268 }) 269 } 270 271 func (s *DestroySuite) TestDestroyDestroyReleaseStorageFlagsMutuallyExclusive(c *gc.C) { 272 _, err := s.runDestroyCommand(c, "test2", "-y", "--destroy-storage", "--release-storage") 273 c.Assert(err, gc.ErrorMatches, "--destroy-storage and --release-storage cannot both be specified") 274 } 275 276 func (s *DestroySuite) TestDestroyDestroyStorageFlagUnspecified(c *gc.C) { 277 s.stub.SetErrors(¶ms.Error{Code: params.CodeHasPersistentStorage}) 278 s.api.modelInfoErr = []*params.Error{nil} 279 _, err := s.runDestroyCommand(c, "test2", "-y") 280 c.Assert(err, gc.ErrorMatches, `cannot destroy model "test2" 281 282 The model has persistent storage remaining: 283 2 volumes and 1 filesystem 284 285 To destroy the storage, run the destroy-model 286 command again with the "--destroy-storage" option. 287 288 To release the storage from Juju's management 289 without destroying it, use the "--release-storage" 290 option instead. The storage can then be imported 291 into another Juju model. 292 293 `) 294 } 295 296 func (s *DestroySuite) TestDestroyDestroyStorageFlagUnspecifiedOldController(c *gc.C) { 297 s.api.bestAPIVersion = 3 298 s.storageAPI.storage = []params.StorageDetails{{}} 299 300 _, err := s.runDestroyCommand(c, "test2", "-y") 301 c.Assert(err, gc.ErrorMatches, `cannot destroy model "test2" 302 303 Destroying this model will destroy the storage, but you 304 have not indicated that you want to do that. 305 306 Please run the the command again with --destroy-storage 307 to confirm that you want to destroy the storage along 308 with the model. 309 310 If instead you want to keep the storage, you must first 311 upgrade the controller to version 2.3 or greater. 312 313 `) 314 } 315 316 func (s *DestroySuite) TestDestroyDestroyStorageFlagUnspecifiedOldControllerNoStorage(c *gc.C) { 317 s.api.bestAPIVersion = 3 318 s.storageAPI.storage = nil // no storage in model 319 320 _, err := s.runDestroyCommand(c, "test2", "-y") 321 c.Assert(err, jc.ErrorIsNil) 322 } 323 324 func (s *DestroySuite) resetModel(c *gc.C) { 325 s.store.Models["test1"] = &jujuclient.ControllerModels{ 326 Models: map[string]jujuclient.ModelDetails{ 327 "admin/test1": {ModelUUID: "test1-uuid", ModelType: coremodel.IAAS}, 328 "admin/test2": {ModelUUID: "test2-uuid", ModelType: coremodel.IAAS}, 329 }, 330 } 331 } 332 333 func (s *DestroySuite) TestDestroyCommandConfirmation(c *gc.C) { 334 var stdin, stdout bytes.Buffer 335 ctx, err := cmd.DefaultContext() 336 c.Assert(err, jc.ErrorIsNil) 337 ctx.Stdout = &stdout 338 ctx.Stdin = &stdin 339 340 // Ensure confirmation is requested if "-y" is not specified. 341 stdin.WriteString("n") 342 _, errc := cmdtest.RunCommandWithDummyProvider(ctx, s.NewDestroyCommand(), "test2") 343 select { 344 case err := <-errc: 345 c.Check(err, gc.ErrorMatches, "model destruction: aborted") 346 case <-time.After(testing.LongWait): 347 c.Fatalf("command took too long") 348 } 349 c.Check(cmdtesting.Stdout(ctx), gc.Matches, "WARNING!.*test2(.|\n)*") 350 checkModelExistsInStore(c, "test1:admin/test1", s.store) 351 352 // EOF on stdin: equivalent to answering no. 353 stdin.Reset() 354 stdout.Reset() 355 _, errc = cmdtest.RunCommandWithDummyProvider(ctx, s.NewDestroyCommand(), "test2") 356 select { 357 case err := <-errc: 358 c.Check(err, gc.ErrorMatches, "model destruction: aborted") 359 case <-time.After(testing.LongWait): 360 c.Fatalf("command took too long") 361 } 362 c.Check(cmdtesting.Stdout(ctx), gc.Matches, "WARNING!.*test2(.|\n)*") 363 checkModelExistsInStore(c, "test1:admin/test2", s.store) 364 365 for _, answer := range []string{"y", "Y", "yes", "YES"} { 366 stdin.Reset() 367 stdout.Reset() 368 stdin.WriteString(answer) 369 _, errc = cmdtest.RunCommandWithDummyProvider(ctx, s.NewDestroyCommand(), "test2") 370 select { 371 case err := <-errc: 372 c.Check(err, jc.ErrorIsNil) 373 case <-time.After(testing.LongWait): 374 c.Fatalf("command took too long") 375 } 376 checkModelRemovedFromStore(c, "test1:admin/test2", s.store) 377 378 // Add the test2 model back into the store for the next test 379 s.resetModel(c) 380 } 381 } 382 383 func (s *DestroySuite) TestDestroyCommandWait(c *gc.C) { 384 checkModelExistsInStore(c, "test1:admin/test2", s.store) 385 386 s.api.modelInfoErr = []*params.Error{nil, nil} 387 s.api.modelStatusPayload = []base.ModelStatus{{ 388 ApplicationCount: 2, 389 HostedMachineCount: 1, 390 Volumes: []base.Volume{ 391 {Detachable: true, Status: "error", Message: "failed to destroy volume 0", Id: "0"}, 392 {Detachable: true, Status: "error", Message: "failed to destroy volume 1", Id: "1"}, 393 {Detachable: true, Status: "error", Message: "failed to destroy volume 2", Id: "2"}, 394 }, 395 Filesystems: []base.Filesystem{ 396 {Detachable: true, Status: "error", Message: "failed to destroy filesystem 0", Id: "0"}, 397 {Detachable: true, Status: "error", Message: "failed to destroy filesystem 1", Id: "1"}, 398 }, 399 }} 400 401 done := make(chan struct{}, 1) 402 outErr := make(chan error, 1) 403 outStdOut := make(chan string, 1) 404 outStdErr := make(chan string, 1) 405 406 go func() { 407 // run destroy model cmd, and timeout in 3s. 408 ctx, err := s.runDestroyCommand(c, "test2", "-y", "-t", "3s") 409 outStdOut <- cmdtesting.Stdout(ctx) 410 outStdErr <- cmdtesting.Stderr(ctx) 411 outErr <- err 412 done <- struct{}{} 413 }() 414 415 c.Assert(s.clock.WaitAdvance(5*time.Second, testing.LongWait, 2), jc.ErrorIsNil) 416 417 select { 418 case <-done: 419 c.Assert(<-outStdErr, gc.Equals, ` 420 Destroying model 421 Waiting on model to be removed, 5 error(s), 1 machine(s), 2 application(s), 3 volume(s), 2 filesystems(s)... 422 Waiting on model to be removed, 5 error(s), 1 machine(s), 2 application(s), 3 volume(s), 2 filesystems(s)... 423 `[1:]) 424 c.Assert(<-outStdOut, gc.Equals, ` 425 426 The following errors were encountered during destroying the model. 427 You can fix the problem causing the errors and run destroy-model again. 428 429 Resource Id Message 430 Filesystem 0 failed to destroy filesystem 0 431 1 failed to destroy filesystem 1 432 Volume 0 failed to destroy volume 0 433 1 failed to destroy volume 1 434 2 failed to destroy volume 2 435 `[1:]) 436 // timeout after 3s. 437 c.Assert(<-outErr, jc.Satisfies, errors.IsTimeout) 438 checkModelExistsInStore(c, "test1:admin/test2", s.store) 439 case <-time.After(testing.LongWait): 440 c.Fatalf("timed out waiting for destroy cmd.") 441 } 442 } 443 444 func (s *DestroySuite) TestBlockedDestroy(c *gc.C) { 445 s.stub.SetErrors(common.OperationBlockedError("TestBlockedDestroy")) 446 _, err := s.runDestroyCommand(c, "test2", "-y") 447 testing.AssertOperationWasBlocked(c, err, ".*TestBlockedDestroy.*") 448 } 449 450 // mockBudgetAPIClient implements the budgetAPIClient interface. 451 type mockBudgetAPIClient struct { 452 *jutesting.Stub 453 } 454 455 func (c *mockBudgetAPIClient) DeleteBudget(model string) (string, error) { 456 c.MethodCall(c, "DeleteBudget", model) 457 return "Budget removed.", c.NextErr() 458 } 459 460 type mockStorageAPI struct { 461 *jutesting.Stub 462 storage []params.StorageDetails 463 } 464 465 func (*mockStorageAPI) Close() error { return nil } 466 467 func (m *mockStorageAPI) ListStorageDetails() ([]params.StorageDetails, error) { 468 m.MethodCall(m, "ListStorageDetails") 469 return m.storage, m.NextErr() 470 }