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 = &params.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(&params.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  }