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