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(¶ms.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 ¶ms.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{}, ¶ms.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 }