github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/cmd/juju/controller/addmodel_test.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package controller_test
     5  
     6  import (
     7  	"fmt"
     8  	"io/ioutil"
     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  	"github.com/juju/utils"
    15  	gc "gopkg.in/check.v1"
    16  	"gopkg.in/juju/names.v2"
    17  	"gopkg.in/yaml.v2"
    18  
    19  	"github.com/juju/juju/api"
    20  	"github.com/juju/juju/api/base"
    21  	"github.com/juju/juju/apiserver/params"
    22  	"github.com/juju/juju/cloud"
    23  	"github.com/juju/juju/cmd/juju/controller"
    24  	"github.com/juju/juju/jujuclient"
    25  	"github.com/juju/juju/jujuclient/jujuclienttesting"
    26  	_ "github.com/juju/juju/provider/ec2"
    27  	"github.com/juju/juju/testing"
    28  )
    29  
    30  type AddModelSuite struct {
    31  	testing.FakeJujuXDGDataHomeSuite
    32  	fakeAddModelAPI *fakeAddClient
    33  	fakeCloudAPI    *fakeCloudAPI
    34  	store           *jujuclienttesting.MemStore
    35  }
    36  
    37  var _ = gc.Suite(&AddModelSuite{})
    38  
    39  func (s *AddModelSuite) SetUpTest(c *gc.C) {
    40  	s.FakeJujuXDGDataHomeSuite.SetUpTest(c)
    41  	s.fakeAddModelAPI = &fakeAddClient{
    42  		model: base.ModelInfo{
    43  			Name:  "test",
    44  			UUID:  "fake-model-uuid",
    45  			Owner: "ignored-for-now",
    46  		},
    47  	}
    48  	s.fakeCloudAPI = &fakeCloudAPI{}
    49  
    50  	// Set up the current controller, and write just enough info
    51  	// so we don't try to refresh
    52  	controllerName := "test-master"
    53  	s.store = jujuclienttesting.NewMemStore()
    54  	s.store.CurrentControllerName = controllerName
    55  	s.store.Controllers[controllerName] = jujuclient.ControllerDetails{}
    56  	s.store.Accounts[controllerName] = jujuclient.AccountDetails{
    57  		User: "bob@local",
    58  	}
    59  	s.store.Credentials["aws"] = cloud.CloudCredential{
    60  		AuthCredentials: map[string]cloud.Credential{
    61  			"secrets": cloud.NewCredential(cloud.AccessKeyAuthType, map[string]string{
    62  				"access-key": "key",
    63  				"secret-key": "sekret",
    64  			}),
    65  		},
    66  	}
    67  }
    68  
    69  type fakeAPIConnection struct {
    70  	api.Connection
    71  }
    72  
    73  func (*fakeAPIConnection) Close() error {
    74  	return nil
    75  }
    76  
    77  func (s *AddModelSuite) run(c *gc.C, args ...string) (*cmd.Context, error) {
    78  	command, _ := controller.NewAddModelCommandForTest(&fakeAPIConnection{}, s.fakeAddModelAPI, s.fakeCloudAPI, s.store)
    79  	return testing.RunCommand(c, command, args...)
    80  }
    81  
    82  func (s *AddModelSuite) TestInit(c *gc.C) {
    83  	modelNameErr := "%q is not a valid name: model names may only contain lowercase letters, digits and hyphens"
    84  	for i, test := range []struct {
    85  		args        []string
    86  		err         string
    87  		name        string
    88  		owner       string
    89  		cloudRegion string
    90  		values      map[string]interface{}
    91  	}{
    92  		{
    93  			err: "model name is required",
    94  		}, {
    95  			args: []string{"new-model"},
    96  			name: "new-model",
    97  		}, {
    98  			args: []string{"n"},
    99  			name: "n",
   100  		}, {
   101  			args: []string{"new model"},
   102  			err:  fmt.Sprintf(modelNameErr, "new model"),
   103  		}, {
   104  			args: []string{"newModel"},
   105  			err:  fmt.Sprintf(modelNameErr, "newModel"),
   106  		}, {
   107  			args: []string{"-"},
   108  			err:  fmt.Sprintf(modelNameErr, "-"),
   109  		}, {
   110  			args: []string{"new@model"},
   111  			err:  fmt.Sprintf(modelNameErr, "new@model"),
   112  		}, {
   113  			args:  []string{"new-model", "--owner", "foo"},
   114  			name:  "new-model",
   115  			owner: "foo",
   116  		}, {
   117  			args: []string{"new-model", "--owner", "not=valid"},
   118  			err:  `"not=valid" is not a valid user`,
   119  		}, {
   120  			args:   []string{"new-model", "--config", "key=value", "--config", "key2=value2"},
   121  			name:   "new-model",
   122  			values: map[string]interface{}{"key": "value", "key2": "value2"},
   123  		}, {
   124  			args:        []string{"new-model", "cloud/region"},
   125  			name:        "new-model",
   126  			cloudRegion: "cloud/region",
   127  		}, {
   128  			args: []string{"new-model", "cloud/region", "extra", "args"},
   129  			err:  `unrecognized args: \["extra" "args"\]`,
   130  		},
   131  	} {
   132  		c.Logf("test %d", i)
   133  		wrappedCommand, command := controller.NewAddModelCommandForTest(nil, nil, nil, s.store)
   134  		err := testing.InitCommand(wrappedCommand, test.args)
   135  		if test.err != "" {
   136  			c.Assert(err, gc.ErrorMatches, test.err)
   137  			continue
   138  		}
   139  
   140  		c.Assert(err, jc.ErrorIsNil)
   141  		c.Assert(command.Name, gc.Equals, test.name)
   142  		c.Assert(command.Owner, gc.Equals, test.owner)
   143  		c.Assert(command.CloudRegion, gc.Equals, test.cloudRegion)
   144  		attrs, err := command.Config.ReadAttrs(nil)
   145  		c.Assert(err, jc.ErrorIsNil)
   146  		if len(test.values) == 0 {
   147  			c.Assert(attrs, gc.HasLen, 0)
   148  		} else {
   149  			c.Assert(attrs, jc.DeepEquals, test.values)
   150  		}
   151  	}
   152  }
   153  
   154  func (s *AddModelSuite) TestAddExistingName(c *gc.C) {
   155  	// If there's any model details existing, we just overwrite them. The
   156  	// controller will error out if the model already exists. Overwriting
   157  	// means we'll replace any stale details from an previously existing
   158  	// model with the same name.
   159  	err := s.store.UpdateModel("test-master", "bob@local/test", jujuclient.ModelDetails{
   160  		"stale-uuid",
   161  	})
   162  	c.Assert(err, jc.ErrorIsNil)
   163  
   164  	_, err = s.run(c, "test")
   165  	c.Assert(err, jc.ErrorIsNil)
   166  
   167  	details, err := s.store.ModelByName("test-master", "bob@local/test")
   168  	c.Assert(err, jc.ErrorIsNil)
   169  	c.Assert(details, jc.DeepEquals, &jujuclient.ModelDetails{"fake-model-uuid"})
   170  }
   171  
   172  func (s *AddModelSuite) TestCredentialsPassedThrough(c *gc.C) {
   173  	_, err := s.run(c, "test", "--credential", "secrets")
   174  	c.Assert(err, jc.ErrorIsNil)
   175  
   176  	c.Assert(s.fakeAddModelAPI.cloudCredential, gc.Equals, names.NewCloudCredentialTag("aws/bob@local/secrets"))
   177  }
   178  
   179  func (s *AddModelSuite) TestCredentialsOtherUserPassedThrough(c *gc.C) {
   180  	_, err := s.run(c, "test", "--credential", "other/secrets")
   181  	c.Assert(err, jc.ErrorIsNil)
   182  
   183  	c.Assert(s.fakeAddModelAPI.cloudCredential, gc.Equals, names.NewCloudCredentialTag("aws/other/secrets"))
   184  }
   185  
   186  func (s *AddModelSuite) TestCredentialsOtherUserPassedThroughWhenCloud(c *gc.C) {
   187  	_, err := s.run(c, "test", "--credential", "other/secrets", "aws/us-west-1")
   188  	c.Assert(err, jc.ErrorIsNil)
   189  
   190  	c.Assert(s.fakeAddModelAPI.cloudCredential, gc.Equals, names.NewCloudCredentialTag("aws/other/secrets"))
   191  }
   192  
   193  func (s *AddModelSuite) TestCredentialsNoDefaultCloud(c *gc.C) {
   194  	s.fakeCloudAPI.SetErrors(&params.Error{Code: params.CodeNotFound})
   195  	_, err := s.run(c, "test", "--credential", "secrets")
   196  	c.Assert(err, gc.ErrorMatches, `there is no default cloud defined, please specify one using:
   197  
   198      juju add-model \[flags\] \<model-name\> cloud\[/region\]`)
   199  }
   200  
   201  func (s *AddModelSuite) TestCloudRegionPassedThrough(c *gc.C) {
   202  	_, err := s.run(c, "test", "aws/us-west-1")
   203  	c.Assert(err, jc.ErrorIsNil)
   204  
   205  	c.Assert(s.fakeAddModelAPI.cloudName, gc.Equals, "aws")
   206  	c.Assert(s.fakeAddModelAPI.cloudRegion, gc.Equals, "us-west-1")
   207  }
   208  
   209  func (s *AddModelSuite) TestDefaultCloudPassedThrough(c *gc.C) {
   210  	_, err := s.run(c, "test")
   211  	c.Assert(err, jc.ErrorIsNil)
   212  
   213  	s.fakeCloudAPI.CheckCallNames(c /*none*/)
   214  	c.Assert(s.fakeAddModelAPI.cloudName, gc.Equals, "")
   215  	c.Assert(s.fakeAddModelAPI.cloudRegion, gc.Equals, "")
   216  }
   217  
   218  func (s *AddModelSuite) TestDefaultCloudRegionPassedThrough(c *gc.C) {
   219  	_, err := s.run(c, "test", "us-west-1")
   220  	c.Assert(err, jc.ErrorIsNil)
   221  
   222  	s.fakeCloudAPI.CheckCalls(c, []gitjujutesting.StubCall{
   223  		{"Cloud", []interface{}{names.NewCloudTag("us-west-1")}},
   224  		{"DefaultCloud", nil},
   225  		{"Cloud", []interface{}{names.NewCloudTag("aws")}},
   226  	})
   227  	c.Assert(s.fakeAddModelAPI.cloudName, gc.Equals, "aws")
   228  	c.Assert(s.fakeAddModelAPI.cloudRegion, gc.Equals, "us-west-1")
   229  }
   230  
   231  func (s *AddModelSuite) TestNoDefaultCloudRegion(c *gc.C) {
   232  	s.fakeCloudAPI.SetErrors(
   233  		&params.Error{Code: params.CodeNotFound}, // no default region
   234  	)
   235  	_, err := s.run(c, "test", "us-west-1")
   236  	c.Assert(err, gc.ErrorMatches, `
   237  "us-west-1" is not a cloud supported by this controller,
   238  and there is no default cloud. The clouds/regions supported
   239  by this controller are:
   240  
   241  CLOUD  REGIONS
   242  aws    us-east-1, us-west-1
   243  lxd    
   244  `[1:])
   245  	s.fakeCloudAPI.CheckCalls(c, []gitjujutesting.StubCall{
   246  		{"Cloud", []interface{}{names.NewCloudTag("us-west-1")}},
   247  		{"DefaultCloud", nil},
   248  		{"Clouds", nil},
   249  	})
   250  }
   251  
   252  func (s *AddModelSuite) TestCloudDefaultRegionPassedThrough(c *gc.C) {
   253  	_, err := s.run(c, "test", "aws")
   254  	c.Assert(err, jc.ErrorIsNil)
   255  
   256  	c.Assert(s.fakeAddModelAPI.cloudName, gc.Equals, "aws")
   257  	c.Assert(s.fakeAddModelAPI.cloudRegion, gc.Equals, "us-east-1")
   258  }
   259  
   260  func (s *AddModelSuite) TestInvalidCloudOrRegionName(c *gc.C) {
   261  	_, err := s.run(c, "test", "oro")
   262  	c.Assert(err, gc.ErrorMatches, `
   263  "oro" is neither a cloud supported by this controller,
   264  nor a region in the controller's default cloud "aws".
   265  The clouds/regions supported by this controller are:
   266  
   267  CLOUD  REGIONS
   268  aws    us-east-1, us-west-1
   269  lxd    
   270  `[1:])
   271  }
   272  
   273  func (s *AddModelSuite) TestComandLineConfigPassedThrough(c *gc.C) {
   274  	_, err := s.run(c, "test", "--config", "account=magic", "--config", "cloud=special")
   275  	c.Assert(err, jc.ErrorIsNil)
   276  
   277  	c.Assert(s.fakeAddModelAPI.config["account"], gc.Equals, "magic")
   278  	c.Assert(s.fakeAddModelAPI.config["cloud"], gc.Equals, "special")
   279  }
   280  
   281  func (s *AddModelSuite) TestConfigFileValuesPassedThrough(c *gc.C) {
   282  	config := map[string]string{
   283  		"account": "magic",
   284  		"cloud":   "9",
   285  	}
   286  	bytes, err := yaml.Marshal(config)
   287  	c.Assert(err, jc.ErrorIsNil)
   288  	file, err := ioutil.TempFile(c.MkDir(), "")
   289  	c.Assert(err, jc.ErrorIsNil)
   290  	file.Write(bytes)
   291  	file.Close()
   292  
   293  	_, err = s.run(c, "test", "--config", file.Name())
   294  	c.Assert(err, jc.ErrorIsNil)
   295  	c.Assert(s.fakeAddModelAPI.config["account"], gc.Equals, "magic")
   296  	c.Assert(s.fakeAddModelAPI.config["cloud"], gc.Equals, "9")
   297  }
   298  
   299  func (s *AddModelSuite) TestConfigFileWithNestedMaps(c *gc.C) {
   300  	nestedConfig := map[string]interface{}{
   301  		"account": "magic",
   302  		"cloud":   "9",
   303  	}
   304  	config := map[string]interface{}{
   305  		"foo":    "bar",
   306  		"nested": nestedConfig,
   307  	}
   308  
   309  	bytes, err := yaml.Marshal(config)
   310  	c.Assert(err, jc.ErrorIsNil)
   311  	file, err := ioutil.TempFile(c.MkDir(), "")
   312  	c.Assert(err, jc.ErrorIsNil)
   313  	file.Write(bytes)
   314  	file.Close()
   315  
   316  	_, err = s.run(c, "test", "--config", file.Name())
   317  	c.Assert(err, jc.ErrorIsNil)
   318  	c.Assert(s.fakeAddModelAPI.config["foo"], gc.Equals, "bar")
   319  	c.Assert(s.fakeAddModelAPI.config["nested"], jc.DeepEquals, nestedConfig)
   320  }
   321  
   322  func (s *AddModelSuite) TestConfigFileFailsToConform(c *gc.C) {
   323  	nestedConfig := map[int]interface{}{
   324  		9: "9",
   325  	}
   326  	config := map[string]interface{}{
   327  		"foo":    "bar",
   328  		"nested": nestedConfig,
   329  	}
   330  	bytes, err := yaml.Marshal(config)
   331  	c.Assert(err, jc.ErrorIsNil)
   332  	file, err := ioutil.TempFile(c.MkDir(), "")
   333  	c.Assert(err, jc.ErrorIsNil)
   334  	file.Write(bytes)
   335  	file.Close()
   336  
   337  	_, err = s.run(c, "test", "--config", file.Name())
   338  	c.Assert(err, gc.ErrorMatches, `unable to parse config: map keyed with non-string value`)
   339  }
   340  
   341  func (s *AddModelSuite) TestConfigFileFormatError(c *gc.C) {
   342  	file, err := ioutil.TempFile(c.MkDir(), "")
   343  	c.Assert(err, jc.ErrorIsNil)
   344  	file.Write(([]byte)("not: valid: yaml"))
   345  	file.Close()
   346  
   347  	_, err = s.run(c, "test", "--config", file.Name())
   348  	c.Assert(err, gc.ErrorMatches, `unable to parse config: yaml: .*`)
   349  }
   350  
   351  func (s *AddModelSuite) TestConfigFileDoesntExist(c *gc.C) {
   352  	_, err := s.run(c, "test", "--config", "missing-file")
   353  	errMsg := ".*" + utils.NoSuchFileErrRegexp
   354  	c.Assert(err, gc.ErrorMatches, errMsg)
   355  }
   356  
   357  func (s *AddModelSuite) TestConfigValuePrecedence(c *gc.C) {
   358  	config := map[string]string{
   359  		"account": "magic",
   360  		"cloud":   "9",
   361  	}
   362  	bytes, err := yaml.Marshal(config)
   363  	c.Assert(err, jc.ErrorIsNil)
   364  	file, err := ioutil.TempFile(c.MkDir(), "")
   365  	c.Assert(err, jc.ErrorIsNil)
   366  	file.Write(bytes)
   367  	file.Close()
   368  
   369  	_, err = s.run(c, "test", "--config", file.Name(), "--config", "account=magic", "--config", "cloud=special")
   370  	c.Assert(err, jc.ErrorIsNil)
   371  	c.Assert(s.fakeAddModelAPI.config["account"], gc.Equals, "magic")
   372  	c.Assert(s.fakeAddModelAPI.config["cloud"], gc.Equals, "special")
   373  }
   374  
   375  func (s *AddModelSuite) TestAddErrorRemoveConfigstoreInfo(c *gc.C) {
   376  	s.fakeAddModelAPI.err = errors.New("bah humbug")
   377  
   378  	_, err := s.run(c, "test")
   379  	c.Assert(err, gc.ErrorMatches, "bah humbug")
   380  
   381  	_, err = s.store.ModelByName("test-master", "bob@local/test")
   382  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   383  }
   384  
   385  func (s *AddModelSuite) TestAddStoresValues(c *gc.C) {
   386  	_, err := s.run(c, "test")
   387  	c.Assert(err, jc.ErrorIsNil)
   388  
   389  	model, err := s.store.ModelByName("test-master", "bob@local/test")
   390  	c.Assert(err, jc.ErrorIsNil)
   391  	c.Assert(model, jc.DeepEquals, &jujuclient.ModelDetails{"fake-model-uuid"})
   392  }
   393  
   394  func (s *AddModelSuite) TestNoEnvCacheOtherUser(c *gc.C) {
   395  	_, err := s.run(c, "test", "--owner", "zeus")
   396  	c.Assert(err, jc.ErrorIsNil)
   397  
   398  	// Creating a model for another user does not update the model cache.
   399  	_, err = s.store.ModelByName("test-master", "bob@local/test")
   400  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   401  }
   402  
   403  // fakeAddClient is used to mock out the behavior of the real
   404  // AddModel command.
   405  type fakeAddClient struct {
   406  	owner           string
   407  	cloudName       string
   408  	cloudRegion     string
   409  	cloudCredential names.CloudCredentialTag
   410  	config          map[string]interface{}
   411  	err             error
   412  	model           base.ModelInfo
   413  }
   414  
   415  var _ controller.AddModelAPI = (*fakeAddClient)(nil)
   416  
   417  func (*fakeAddClient) Close() error {
   418  	return nil
   419  }
   420  
   421  func (f *fakeAddClient) CreateModel(name, owner, cloudName, cloudRegion string, cloudCredential names.CloudCredentialTag, config map[string]interface{}) (base.ModelInfo, error) {
   422  	if f.err != nil {
   423  		return base.ModelInfo{}, f.err
   424  	}
   425  	f.owner = owner
   426  	f.cloudCredential = cloudCredential
   427  	f.cloudName = cloudName
   428  	f.cloudRegion = cloudRegion
   429  	f.config = config
   430  	return f.model, nil
   431  }
   432  
   433  // TODO(wallyworld) - improve this stub and add test asserts
   434  type fakeCloudAPI struct {
   435  	controller.CloudAPI
   436  	gitjujutesting.Stub
   437  }
   438  
   439  func (c *fakeCloudAPI) DefaultCloud() (names.CloudTag, error) {
   440  	c.MethodCall(c, "DefaultCloud")
   441  	return names.NewCloudTag("aws"), c.NextErr()
   442  }
   443  
   444  func (c *fakeCloudAPI) Clouds() (map[names.CloudTag]cloud.Cloud, error) {
   445  	c.MethodCall(c, "Clouds")
   446  	return map[names.CloudTag]cloud.Cloud{
   447  		names.NewCloudTag("aws"): {
   448  			Regions: []cloud.Region{
   449  				{Name: "us-east-1"},
   450  				{Name: "us-west-1"},
   451  			},
   452  		},
   453  		names.NewCloudTag("lxd"): {},
   454  	}, c.NextErr()
   455  }
   456  
   457  func (c *fakeCloudAPI) Cloud(tag names.CloudTag) (cloud.Cloud, error) {
   458  	c.MethodCall(c, "Cloud", tag)
   459  	if tag.Id() != "aws" {
   460  		return cloud.Cloud{}, &params.Error{Code: params.CodeNotFound}
   461  	}
   462  	return cloud.Cloud{
   463  		Type:      "ec2",
   464  		AuthTypes: []cloud.AuthType{cloud.AccessKeyAuthType},
   465  		Regions: []cloud.Region{
   466  			{Name: "us-east-1"},
   467  			{Name: "us-west-1"},
   468  		},
   469  	}, c.NextErr()
   470  }
   471  
   472  func (c *fakeCloudAPI) UserCredentials(user names.UserTag, cloud names.CloudTag) ([]names.CloudCredentialTag, error) {
   473  	c.MethodCall(c, "UserCredentials", user, cloud)
   474  	return []names.CloudCredentialTag{
   475  		names.NewCloudCredentialTag("cloud/admin@local/default"),
   476  		names.NewCloudCredentialTag("aws/other@local/secrets"),
   477  	}, c.NextErr()
   478  }
   479  
   480  func (c *fakeCloudAPI) UpdateCredential(credentialTag names.CloudCredentialTag, credential cloud.Credential) error {
   481  	c.MethodCall(c, "UpdateCredential", credentialTag, credential)
   482  	return c.NextErr()
   483  }