github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/juju/apiconn_test.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package juju_test 5 6 import ( 7 "fmt" 8 "os" 9 "time" 10 11 gc "launchpad.net/gocheck" 12 13 "launchpad.net/juju-core/constraints" 14 "launchpad.net/juju-core/environs" 15 "launchpad.net/juju-core/environs/bootstrap" 16 "launchpad.net/juju-core/environs/config" 17 "launchpad.net/juju-core/environs/configstore" 18 envtesting "launchpad.net/juju-core/environs/testing" 19 "launchpad.net/juju-core/juju" 20 "launchpad.net/juju-core/juju/osenv" 21 "launchpad.net/juju-core/provider/dummy" 22 "launchpad.net/juju-core/state/api" 23 coretesting "launchpad.net/juju-core/testing" 24 jc "launchpad.net/juju-core/testing/checkers" 25 "launchpad.net/juju-core/testing/testbase" 26 ) 27 28 type NewAPIConnSuite struct { 29 testbase.LoggingSuite 30 envtesting.ToolsFixture 31 } 32 33 var _ = gc.Suite(&NewAPIConnSuite{}) 34 35 func (cs *NewAPIConnSuite) SetUpTest(c *gc.C) { 36 cs.LoggingSuite.SetUpTest(c) 37 cs.ToolsFixture.SetUpTest(c) 38 } 39 40 func (cs *NewAPIConnSuite) TearDownTest(c *gc.C) { 41 dummy.Reset() 42 cs.ToolsFixture.TearDownTest(c) 43 cs.LoggingSuite.TearDownTest(c) 44 } 45 46 func (*NewAPIConnSuite) TestNewConn(c *gc.C) { 47 cfg, err := config.New(config.NoDefaults, dummy.SampleConfig()) 48 c.Assert(err, gc.IsNil) 49 ctx := coretesting.Context(c) 50 env, err := environs.Prepare(cfg, ctx, configstore.NewMem()) 51 c.Assert(err, gc.IsNil) 52 53 envtesting.UploadFakeTools(c, env.Storage()) 54 err = bootstrap.Bootstrap(ctx, env, constraints.Value{}) 55 c.Assert(err, gc.IsNil) 56 57 cfg = env.Config() 58 cfg, err = cfg.Apply(map[string]interface{}{ 59 "secret": "fnord", 60 }) 61 c.Assert(err, gc.IsNil) 62 err = env.SetConfig(cfg) 63 c.Assert(err, gc.IsNil) 64 65 conn, err := juju.NewAPIConn(env, api.DefaultDialOpts()) 66 c.Assert(err, gc.IsNil) 67 c.Assert(conn.Environ, gc.Equals, env) 68 c.Assert(conn.State, gc.NotNil) 69 70 // the secrets will not be updated, as they already exist 71 attrs, err := conn.State.Client().EnvironmentGet() 72 c.Assert(attrs["secret"], gc.Equals, "pork") 73 74 c.Assert(conn.Close(), gc.IsNil) 75 } 76 77 type NewAPIClientSuite struct { 78 testbase.LoggingSuite 79 } 80 81 var _ = gc.Suite(&NewAPIClientSuite{}) 82 83 func (cs *NewAPIClientSuite) TearDownTest(c *gc.C) { 84 dummy.Reset() 85 cs.LoggingSuite.TearDownTest(c) 86 } 87 88 func (*NewAPIClientSuite) TestNameDefault(c *gc.C) { 89 defer coretesting.MakeMultipleEnvHome(c).Restore() 90 // The connection logic should not delay the config connection 91 // at all when there is no environment info available. 92 // Make sure of that by providing a suitably long delay 93 // and checking that the connection happens within that 94 // time. 95 defer testbase.PatchValue(juju.ProviderConnectDelay, coretesting.LongWait).Restore() 96 bootstrapEnv(c, coretesting.SampleEnvName, defaultConfigStore(c)) 97 98 startTime := time.Now() 99 apiclient, err := juju.NewAPIClientFromName("") 100 c.Assert(err, gc.IsNil) 101 defer apiclient.Close() 102 c.Assert(time.Since(startTime), jc.LessThan, coretesting.LongWait) 103 104 // We should get the default sample environment if we ask for "" 105 assertEnvironmentName(c, apiclient, coretesting.SampleEnvName) 106 } 107 108 func (*NewAPIClientSuite) TestNameNotDefault(c *gc.C) { 109 defer coretesting.MakeMultipleEnvHome(c).Restore() 110 envName := coretesting.SampleCertName + "-2" 111 bootstrapEnv(c, envName, defaultConfigStore(c)) 112 apiclient, err := juju.NewAPIClientFromName(envName) 113 c.Assert(err, gc.IsNil) 114 defer apiclient.Close() 115 assertEnvironmentName(c, apiclient, envName) 116 } 117 118 func (*NewAPIClientSuite) TestWithInfoOnly(c *gc.C) { 119 defer coretesting.MakeEmptyFakeHome(c).Restore() 120 storeConfig := &environInfo{ 121 creds: configstore.APICredentials{ 122 User: "foo", 123 Password: "foopass", 124 }, 125 endpoint: configstore.APIEndpoint{ 126 Addresses: []string{"foo.invalid"}, 127 CACert: "certificated", 128 }, 129 } 130 store := newConfigStore("noconfig", storeConfig) 131 132 called := 0 133 expectState := new(api.State) 134 apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (*api.State, error) { 135 c.Check(apiInfo.Tag, gc.Equals, "user-foo") 136 c.Check(string(apiInfo.CACert), gc.Equals, "certificated") 137 c.Check(apiInfo.Password, gc.Equals, "foopass") 138 c.Check(opts, gc.DeepEquals, api.DefaultDialOpts()) 139 called++ 140 return expectState, nil 141 } 142 // Give NewAPIFromName a store interface that can report when the 143 // config was written to, to ensure the cache isn't updated. 144 defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore() 145 mockStore := &storageWithWriteNotify{store: store} 146 st, err := juju.NewAPIFromName("noconfig", mockStore) 147 c.Assert(err, gc.IsNil) 148 c.Assert(st, gc.Equals, expectState) 149 c.Assert(called, gc.Equals, 1) 150 c.Assert(mockStore.written, jc.IsFalse) 151 } 152 153 func (*NewAPIClientSuite) TestWithConfigAndNoInfo(c *gc.C) { 154 defer coretesting.MakeSampleHome(c).Restore() 155 156 store := newConfigStore(coretesting.SampleEnvName, &environInfo{ 157 bootstrapConfig: map[string]interface{}{ 158 "type": "dummy", 159 "name": "myenv", 160 "state-server": true, 161 "authorized-keys": "i-am-a-key", 162 "default-series": config.DefaultSeries, 163 "firewall-mode": config.FwInstance, 164 "development": false, 165 "ssl-hostname-verification": true, 166 "admin-secret": "adminpass", 167 }, 168 }) 169 bootstrapEnv(c, coretesting.SampleEnvName, store) 170 171 // Verify the cache is empty. 172 info, err := store.ReadInfo("myenv") 173 c.Assert(err, gc.IsNil) 174 c.Assert(info, gc.NotNil) 175 c.Assert(info.APIEndpoint(), jc.DeepEquals, configstore.APIEndpoint{}) 176 c.Assert(info.APICredentials(), jc.DeepEquals, configstore.APICredentials{}) 177 178 called := 0 179 expectState := new(api.State) 180 apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (*api.State, error) { 181 c.Check(apiInfo.Tag, gc.Equals, "user-admin") 182 c.Check(string(apiInfo.CACert), gc.Not(gc.Equals), "") 183 c.Check(apiInfo.Password, gc.Equals, "adminpass") 184 c.Check(opts, gc.DeepEquals, api.DefaultDialOpts()) 185 called++ 186 return expectState, nil 187 } 188 defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore() 189 st, err := juju.NewAPIFromName("myenv", store) 190 c.Assert(err, gc.IsNil) 191 c.Assert(st, gc.Equals, expectState) 192 c.Assert(called, gc.Equals, 1) 193 194 // Make sure the cache is updated. 195 info, err = store.ReadInfo("myenv") 196 c.Assert(err, gc.IsNil) 197 c.Assert(info, gc.NotNil) 198 ep := info.APIEndpoint() 199 c.Check(ep.Addresses, gc.HasLen, 1) 200 c.Check(ep.Addresses[0], gc.Matches, `127\.0\.0\.1:\d+`) 201 c.Check(ep.CACert, gc.Not(gc.Equals), "") 202 creds := info.APICredentials() 203 c.Check(creds.User, gc.Equals, "admin") 204 c.Check(creds.Password, gc.Equals, "adminpass") 205 } 206 207 func (*NewAPIClientSuite) TestWithInfoError(c *gc.C) { 208 defer coretesting.MakeEmptyFakeHome(c).Restore() 209 expectErr := fmt.Errorf("an error") 210 store := newConfigStoreWithError(expectErr) 211 defer testbase.PatchValue(juju.APIOpen, panicAPIOpen).Restore() 212 client, err := juju.NewAPIFromName("noconfig", store) 213 c.Assert(err, gc.Equals, expectErr) 214 c.Assert(client, gc.IsNil) 215 } 216 217 func panicAPIOpen(apiInfo *api.Info, opts api.DialOpts) (*api.State, error) { 218 panic("api.Open called unexpectedly") 219 } 220 221 func (*NewAPIClientSuite) TestWithInfoNoAddresses(c *gc.C) { 222 defer coretesting.MakeEmptyFakeHome(c).Restore() 223 store := newConfigStore("noconfig", &environInfo{ 224 endpoint: configstore.APIEndpoint{ 225 Addresses: []string{}, 226 CACert: "certificated", 227 }, 228 }) 229 defer testbase.PatchValue(juju.APIOpen, panicAPIOpen).Restore() 230 231 st, err := juju.NewAPIFromName("noconfig", store) 232 c.Assert(err, gc.ErrorMatches, `environment "noconfig" not found`) 233 c.Assert(st, gc.IsNil) 234 } 235 236 func (*NewAPIClientSuite) TestWithInfoAPIOpenError(c *gc.C) { 237 defer coretesting.MakeEmptyFakeHome(c).Restore() 238 store := newConfigStore("noconfig", &environInfo{ 239 endpoint: configstore.APIEndpoint{ 240 Addresses: []string{"foo.invalid"}, 241 }, 242 }) 243 244 expectErr := fmt.Errorf("an error") 245 apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (*api.State, error) { 246 return nil, expectErr 247 } 248 defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore() 249 st, err := juju.NewAPIFromName("noconfig", store) 250 c.Assert(err, gc.Equals, expectErr) 251 c.Assert(st, gc.IsNil) 252 } 253 254 func (*NewAPIClientSuite) TestWithSlowInfoConnect(c *gc.C) { 255 defer coretesting.MakeSampleHome(c).Restore() 256 store := configstore.NewMem() 257 bootstrapEnv(c, coretesting.SampleEnvName, store) 258 setEndpointAddress(c, store, coretesting.SampleEnvName, "infoapi.invalid") 259 260 infoOpenedState := new(api.State) 261 infoEndpointOpened := make(chan struct{}) 262 cfgOpenedState := new(api.State) 263 // On a sample run with no delay, the logic took 45ms to run, so 264 // we make the delay slightly more than that, so that if the 265 // logic doesn't delay at all, the test will fail reasonably consistently. 266 defer testbase.PatchValue(juju.ProviderConnectDelay, 50*time.Millisecond).Restore() 267 apiOpen := func(info *api.Info, opts api.DialOpts) (*api.State, error) { 268 if info.Addrs[0] == "infoapi.invalid" { 269 infoEndpointOpened <- struct{}{} 270 return infoOpenedState, nil 271 } 272 return cfgOpenedState, nil 273 } 274 defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore() 275 276 stateClosed, restoreAPIClose := setAPIClosed() 277 defer restoreAPIClose.Restore() 278 279 startTime := time.Now() 280 st, err := juju.NewAPIFromName(coretesting.SampleEnvName, store) 281 c.Assert(err, gc.IsNil) 282 // The connection logic should wait for some time before opening 283 // the API from the configuration. 284 c.Assert(time.Since(startTime), jc.GreaterThan, *juju.ProviderConnectDelay) 285 c.Assert(st, gc.Equals, cfgOpenedState) 286 287 select { 288 case <-infoEndpointOpened: 289 case <-time.After(coretesting.LongWait): 290 c.Errorf("api never opened via info") 291 } 292 293 // Check that the ignored state was closed. 294 select { 295 case st := <-stateClosed: 296 c.Assert(st, gc.Equals, infoOpenedState) 297 case <-time.After(coretesting.LongWait): 298 c.Errorf("timed out waiting for state to be closed") 299 } 300 } 301 302 func setEndpointAddress(c *gc.C, store configstore.Storage, envName string, addr string) { 303 // Populate the environment's info with an endpoint 304 // with a known address. 305 info, err := store.ReadInfo(coretesting.SampleEnvName) 306 c.Assert(err, gc.IsNil) 307 info.SetAPIEndpoint(configstore.APIEndpoint{ 308 Addresses: []string{addr}, 309 CACert: "certificated", 310 }) 311 err = info.Write() 312 c.Assert(err, gc.IsNil) 313 } 314 315 func (*NewAPIClientSuite) TestWithSlowConfigConnect(c *gc.C) { 316 defer coretesting.MakeSampleHome(c).Restore() 317 318 store := configstore.NewMem() 319 bootstrapEnv(c, coretesting.SampleEnvName, store) 320 setEndpointAddress(c, store, coretesting.SampleEnvName, "infoapi.invalid") 321 322 infoOpenedState := new(api.State) 323 infoEndpointOpened := make(chan struct{}) 324 cfgOpenedState := new(api.State) 325 cfgEndpointOpened := make(chan struct{}) 326 327 defer testbase.PatchValue(juju.ProviderConnectDelay, 0*time.Second).Restore() 328 apiOpen := func(info *api.Info, opts api.DialOpts) (*api.State, error) { 329 if info.Addrs[0] == "infoapi.invalid" { 330 infoEndpointOpened <- struct{}{} 331 <-infoEndpointOpened 332 return infoOpenedState, nil 333 } 334 cfgEndpointOpened <- struct{}{} 335 <-cfgEndpointOpened 336 return cfgOpenedState, nil 337 } 338 defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore() 339 340 stateClosed, restoreAPIClose := setAPIClosed() 341 defer restoreAPIClose.Restore() 342 343 done := make(chan struct{}) 344 go func() { 345 st, err := juju.NewAPIFromName(coretesting.SampleEnvName, store) 346 c.Check(err, gc.IsNil) 347 c.Check(st, gc.Equals, infoOpenedState) 348 close(done) 349 }() 350 351 // Check that we're trying to connect to both endpoints: 352 select { 353 case <-infoEndpointOpened: 354 case <-time.After(coretesting.LongWait): 355 c.Fatalf("api never opened via info") 356 } 357 select { 358 case <-cfgEndpointOpened: 359 case <-time.After(coretesting.LongWait): 360 c.Fatalf("api never opened via config") 361 } 362 // Let the info endpoint open go ahead and 363 // check that the NewAPIFromName call returns. 364 infoEndpointOpened <- struct{}{} 365 select { 366 case <-done: 367 case <-time.After(coretesting.LongWait): 368 c.Errorf("timed out opening API") 369 } 370 371 // Let the config endpoint open go ahead and 372 // check that its state is closed. 373 cfgEndpointOpened <- struct{}{} 374 select { 375 case st := <-stateClosed: 376 c.Assert(st, gc.Equals, cfgOpenedState) 377 case <-time.After(coretesting.LongWait): 378 c.Errorf("timed out waiting for state to be closed") 379 } 380 } 381 382 func (*NewAPIClientSuite) TestBothError(c *gc.C) { 383 defer coretesting.MakeSampleHome(c).Restore() 384 store := configstore.NewMem() 385 bootstrapEnv(c, coretesting.SampleEnvName, store) 386 setEndpointAddress(c, store, coretesting.SampleEnvName, "infoapi.invalid") 387 388 defer testbase.PatchValue(juju.ProviderConnectDelay, 0*time.Second).Restore() 389 apiOpen := func(info *api.Info, opts api.DialOpts) (*api.State, error) { 390 if info.Addrs[0] == "infoapi.invalid" { 391 return nil, fmt.Errorf("info connect failed") 392 } 393 return nil, fmt.Errorf("config connect failed") 394 } 395 defer testbase.PatchValue(juju.APIOpen, apiOpen).Restore() 396 st, err := juju.NewAPIFromName(coretesting.SampleEnvName, store) 397 c.Check(err, gc.ErrorMatches, "config connect failed") 398 c.Check(st, gc.IsNil) 399 } 400 401 func defaultConfigStore(c *gc.C) configstore.Storage { 402 store, err := configstore.Default() 403 c.Assert(err, gc.IsNil) 404 return store 405 } 406 407 // TODO(jam): 2013-08-27 This should move somewhere in api.* 408 func (*NewAPIClientSuite) TestMultipleCloseOk(c *gc.C) { 409 defer coretesting.MakeSampleHome(c).Restore() 410 bootstrapEnv(c, "", defaultConfigStore(c)) 411 client, _ := juju.NewAPIClientFromName("") 412 c.Assert(client.Close(), gc.IsNil) 413 c.Assert(client.Close(), gc.IsNil) 414 c.Assert(client.Close(), gc.IsNil) 415 } 416 417 func (*NewAPIClientSuite) TestWithBootstrapConfigAndNoEnvironmentsFile(c *gc.C) { 418 defer coretesting.MakeSampleHome(c).Restore() 419 store := configstore.NewMem() 420 bootstrapEnv(c, coretesting.SampleEnvName, store) 421 info, err := store.ReadInfo(coretesting.SampleEnvName) 422 c.Assert(err, gc.IsNil) 423 c.Assert(info.BootstrapConfig(), gc.NotNil) 424 c.Assert(info.APIEndpoint().Addresses, gc.HasLen, 0) 425 426 err = os.Remove(osenv.JujuHomePath("environments.yaml")) 427 c.Assert(err, gc.IsNil) 428 429 st, err := juju.NewAPIFromName(coretesting.SampleEnvName, store) 430 c.Check(err, gc.IsNil) 431 st.Close() 432 } 433 434 func (*NewAPIClientSuite) TestWithBootstrapConfigTakesPrecedence(c *gc.C) { 435 // We want to make sure that the code is using the bootstrap 436 // config rather than information from environments.yaml, 437 // even when there is an entry in environments.yaml 438 // We can do that by changing the info bootstrap config 439 // so it has a different environment name. 440 defer coretesting.MakeMultipleEnvHome(c).Restore() 441 442 store := configstore.NewMem() 443 bootstrapEnv(c, coretesting.SampleEnvName, store) 444 info, err := store.ReadInfo(coretesting.SampleEnvName) 445 c.Assert(err, gc.IsNil) 446 447 envName2 := coretesting.SampleCertName + "-2" 448 info2, err := store.CreateInfo(envName2) 449 c.Assert(err, gc.IsNil) 450 info2.SetBootstrapConfig(info.BootstrapConfig()) 451 err = info2.Write() 452 c.Assert(err, gc.IsNil) 453 454 // Now we have info for envName2 which will actually 455 // cause a connection to the originally bootstrapped 456 // state. 457 st, err := juju.NewAPIFromName(envName2, store) 458 c.Check(err, gc.IsNil) 459 st.Close() 460 461 // Sanity check that connecting to the envName2 462 // but with no info fails. 463 // Currently this panics with an "environment not prepared" error. 464 // Disable for now until an upcoming branch fixes it. 465 // err = info2.Destroy() 466 // c.Assert(err, gc.IsNil) 467 // st, err = juju.NewAPIFromName(envName2, store) 468 // if err == nil { 469 // st.Close() 470 // } 471 // c.Assert(err, gc.ErrorMatches, "fooobie") 472 } 473 474 func assertEnvironmentName(c *gc.C, client *api.Client, expectName string) { 475 envInfo, err := client.EnvironmentInfo() 476 c.Assert(err, gc.IsNil) 477 c.Assert(envInfo.Name, gc.Equals, expectName) 478 } 479 480 func setAPIClosed() (<-chan *api.State, testbase.Restorer) { 481 stateClosed := make(chan *api.State) 482 apiClose := func(st *api.State) error { 483 stateClosed <- st 484 return nil 485 } 486 return stateClosed, testbase.PatchValue(juju.APIClose, apiClose) 487 } 488 489 func updateSecretsNoop(_ environs.Environ, _ *api.State) error { 490 return nil 491 } 492 493 // newConfigStoreWithError that will return the given 494 // error from ReadInfo. 495 func newConfigStoreWithError(err error) configstore.Storage { 496 return &errorConfigStorage{ 497 Storage: configstore.NewMem(), 498 err: err, 499 } 500 } 501 502 type errorConfigStorage struct { 503 configstore.Storage 504 err error 505 } 506 507 func (store *errorConfigStorage) ReadInfo(envName string) (configstore.EnvironInfo, error) { 508 return nil, store.err 509 } 510 511 type environInfo struct { 512 creds configstore.APICredentials 513 endpoint configstore.APIEndpoint 514 bootstrapConfig map[string]interface{} 515 } 516 517 // newConfigStore returns a storage that contains information 518 // for the environment name. 519 func newConfigStore(envName string, info *environInfo) configstore.Storage { 520 store := configstore.NewMem() 521 newInfo, err := store.CreateInfo(envName) 522 if err != nil { 523 panic(err) 524 } 525 newInfo.SetAPICredentials(info.creds) 526 newInfo.SetAPIEndpoint(info.endpoint) 527 newInfo.SetBootstrapConfig(info.bootstrapConfig) 528 err = newInfo.Write() 529 if err != nil { 530 panic(err) 531 } 532 return store 533 } 534 535 type storageWithWriteNotify struct { 536 written bool 537 store configstore.Storage 538 } 539 540 func (*storageWithWriteNotify) CreateInfo(envName string) (configstore.EnvironInfo, error) { 541 panic("CreateInfo not implemented") 542 } 543 544 func (s *storageWithWriteNotify) ReadInfo(envName string) (configstore.EnvironInfo, error) { 545 info, err := s.store.ReadInfo(envName) 546 if err != nil { 547 return nil, err 548 } 549 return &infoWithWriteNotify{ 550 written: &s.written, 551 EnvironInfo: info, 552 }, nil 553 } 554 555 type infoWithWriteNotify struct { 556 configstore.EnvironInfo 557 written *bool 558 } 559 560 func (info *infoWithWriteNotify) Write() error { 561 *info.written = true 562 return info.EnvironInfo.Write() 563 }