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