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