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