github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/api/client_test.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package api_test 5 6 import ( 7 "bytes" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "net" 12 "net/http" 13 "net/url" 14 "path" 15 "strings" 16 17 "github.com/juju/errors" 18 "github.com/juju/httprequest" 19 "github.com/juju/loggo" 20 jc "github.com/juju/testing/checkers" 21 "github.com/juju/version" 22 "golang.org/x/net/websocket" 23 gc "gopkg.in/check.v1" 24 "gopkg.in/juju/charm.v6-unstable" 25 "gopkg.in/juju/names.v2" 26 27 "github.com/juju/juju/api" 28 "github.com/juju/juju/api/base" 29 "github.com/juju/juju/apiserver/params" 30 jujunames "github.com/juju/juju/juju/names" 31 jujutesting "github.com/juju/juju/juju/testing" 32 "github.com/juju/juju/rpc" 33 "github.com/juju/juju/state" 34 "github.com/juju/juju/testcharms" 35 coretesting "github.com/juju/juju/testing" 36 "github.com/juju/juju/testing/factory" 37 ) 38 39 type clientSuite struct { 40 jujutesting.JujuConnSuite 41 } 42 43 var _ = gc.Suite(&clientSuite{}) 44 45 // TODO(jam) 2013-08-27 http://pad.lv/1217282 46 // Right now most of the direct tests for api.Client behavior are in 47 // apiserver/client/*_test.go 48 49 func (s *clientSuite) TestCloseMultipleOk(c *gc.C) { 50 client := s.APIState.Client() 51 c.Assert(client.Close(), gc.IsNil) 52 c.Assert(client.Close(), gc.IsNil) 53 c.Assert(client.Close(), gc.IsNil) 54 } 55 56 func (s *clientSuite) TestUploadToolsOtherModel(c *gc.C) { 57 otherSt, otherAPISt := s.otherModel(c) 58 defer otherSt.Close() 59 defer otherAPISt.Close() 60 client := otherAPISt.Client() 61 newVersion := version.MustParseBinary("5.4.3-quantal-amd64") 62 var called bool 63 64 // build fake tools 65 expectedTools, _ := coretesting.TarGz( 66 coretesting.NewTarFile(jujunames.Jujud, 0777, "jujud contents "+newVersion.String())) 67 68 // UploadTools does not use the facades, so instead of patching the 69 // facade call, we set up a fake endpoint to test. 70 defer fakeAPIEndpoint(c, client, envEndpoint(c, otherAPISt, "tools"), "POST", 71 func(w http.ResponseWriter, r *http.Request) { 72 called = true 73 74 c.Assert(r.URL.Query(), gc.DeepEquals, url.Values{ 75 "binaryVersion": []string{"5.4.3-quantal-amd64"}, 76 "series": []string{""}, 77 }) 78 defer r.Body.Close() 79 obtainedTools, err := ioutil.ReadAll(r.Body) 80 c.Assert(err, jc.ErrorIsNil) 81 c.Assert(obtainedTools, gc.DeepEquals, expectedTools) 82 }, 83 ).Close() 84 85 // We don't test the error or tools results as we only wish to assert that 86 // the API client POSTs the tools archive to the correct endpoint. 87 client.UploadTools(bytes.NewReader(expectedTools), newVersion) 88 c.Assert(called, jc.IsTrue) 89 } 90 91 func (s *clientSuite) TestAddLocalCharm(c *gc.C) { 92 charmArchive := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 93 curl := charm.MustParseURL( 94 fmt.Sprintf("local:quantal/%s-%d", charmArchive.Meta().Name, charmArchive.Revision()), 95 ) 96 client := s.APIState.Client() 97 98 // Test the sanity checks first. 99 _, err := client.AddLocalCharm(charm.MustParseURL("cs:quantal/wordpress-1"), nil) 100 c.Assert(err, gc.ErrorMatches, `expected charm URL with local: schema, got "cs:quantal/wordpress-1"`) 101 102 // Upload an archive with its original revision. 103 savedURL, err := client.AddLocalCharm(curl, charmArchive) 104 c.Assert(err, jc.ErrorIsNil) 105 c.Assert(savedURL.String(), gc.Equals, curl.String()) 106 107 // Upload a charm directory with changed revision. 108 charmDir := testcharms.Repo.ClonedDir(c.MkDir(), "dummy") 109 charmDir.SetDiskRevision(42) 110 savedURL, err = client.AddLocalCharm(curl, charmDir) 111 c.Assert(err, jc.ErrorIsNil) 112 c.Assert(savedURL.Revision, gc.Equals, 42) 113 114 // Upload a charm directory again, revision should be bumped. 115 savedURL, err = client.AddLocalCharm(curl, charmDir) 116 c.Assert(err, jc.ErrorIsNil) 117 c.Assert(savedURL.String(), gc.Equals, curl.WithRevision(43).String()) 118 } 119 120 func (s *clientSuite) TestAddLocalCharmOtherModel(c *gc.C) { 121 charmArchive := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 122 curl := charm.MustParseURL( 123 fmt.Sprintf("local:quantal/%s-%d", charmArchive.Meta().Name, charmArchive.Revision()), 124 ) 125 126 otherSt, otherAPISt := s.otherModel(c) 127 defer otherSt.Close() 128 defer otherAPISt.Close() 129 client := otherAPISt.Client() 130 131 // Upload an archive 132 savedURL, err := client.AddLocalCharm(curl, charmArchive) 133 c.Assert(err, jc.ErrorIsNil) 134 c.Assert(savedURL.String(), gc.Equals, curl.String()) 135 136 charm, err := otherSt.Charm(curl) 137 c.Assert(err, jc.ErrorIsNil) 138 c.Assert(charm.String(), gc.Equals, curl.String()) 139 } 140 141 func (s *clientSuite) otherModel(c *gc.C) (*state.State, api.Connection) { 142 otherSt := s.Factory.MakeModel(c, nil) 143 info := s.APIInfo(c) 144 info.ModelTag = otherSt.ModelTag() 145 apiState, err := api.Open(info, api.DefaultDialOpts()) 146 c.Assert(err, jc.ErrorIsNil) 147 return otherSt, apiState 148 } 149 150 func (s *clientSuite) TestAddLocalCharmError(c *gc.C) { 151 client := s.APIState.Client() 152 153 // AddLocalCharm does not use the facades, so instead of patching the 154 // facade call, we set up a fake endpoint to test. 155 defer fakeAPIEndpoint(c, client, envEndpoint(c, s.APIState, "charms"), "POST", 156 func(w http.ResponseWriter, r *http.Request) { 157 httprequest.WriteJSON(w, http.StatusMethodNotAllowed, ¶ms.CharmsResponse{ 158 Error: "the POST method is not allowed", 159 ErrorCode: params.CodeMethodNotAllowed, 160 }) 161 }, 162 ).Close() 163 164 charmArchive := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 165 curl := charm.MustParseURL( 166 fmt.Sprintf("local:quantal/%s-%d", charmArchive.Meta().Name, charmArchive.Revision()), 167 ) 168 169 _, err := client.AddLocalCharm(curl, charmArchive) 170 c.Assert(err, gc.ErrorMatches, `POST http://.+: the POST method is not allowed`) 171 } 172 173 func (s *clientSuite) TestMinVersionLocalCharm(c *gc.C) { 174 tests := []minverTest{ 175 {"2.0.0", "1.0.0", true}, 176 {"1.0.0", "2.0.0", false}, 177 {"1.25.0", "1.24.0", true}, 178 {"1.24.0", "1.25.0", false}, 179 {"1.25.1", "1.25.0", true}, 180 {"1.25.0", "1.25.1", false}, 181 {"1.25.0", "1.25.0", true}, 182 {"1.25.0", "1.25-alpha1", true}, 183 {"1.25-alpha1", "1.25.0", false}, 184 } 185 client := s.APIState.Client() 186 for _, t := range tests { 187 testMinVer(client, t, c) 188 } 189 } 190 191 type minverTest struct { 192 juju string 193 charm string 194 ok bool 195 } 196 197 func testMinVer(client *api.Client, t minverTest, c *gc.C) { 198 charmMinVer := version.MustParse(t.charm) 199 jujuVer := version.MustParse(t.juju) 200 201 cleanup := api.PatchClientFacadeCall(client, 202 func(request string, paramsIn interface{}, response interface{}) error { 203 c.Assert(paramsIn, gc.IsNil) 204 if response, ok := response.(*params.AgentVersionResult); ok { 205 response.Version = jujuVer 206 } else { 207 c.Log("wrong output structure") 208 c.Fail() 209 } 210 return nil 211 }, 212 ) 213 defer cleanup() 214 215 charmArchive := testcharms.Repo.CharmArchive(c.MkDir(), "dummy") 216 curl := charm.MustParseURL( 217 fmt.Sprintf("local:quantal/%s-%d", charmArchive.Meta().Name, charmArchive.Revision()), 218 ) 219 charmArchive.Meta().MinJujuVersion = charmMinVer 220 221 _, err := client.AddLocalCharm(curl, charmArchive) 222 223 if t.ok { 224 if err != nil { 225 c.Errorf("Unexpected non-nil error for jujuver %v, minver %v: %#v", t.juju, t.charm, err) 226 } 227 } else { 228 if err == nil { 229 c.Errorf("Unexpected nil error for jujuver %v, minver %v", t.juju, t.charm) 230 } else if !api.IsMinVersionError(err) { 231 c.Errorf("Wrong error for jujuver %v, minver %v: expected minVersionError, got: %#v", t.juju, t.charm, err) 232 } 233 } 234 } 235 236 func (s *clientSuite) TestOpenURIFound(c *gc.C) { 237 // Use tools download to test OpenURI 238 const toolsVersion = "2.0.0-xenial-ppc64" 239 s.AddToolsToState(c, version.MustParseBinary(toolsVersion)) 240 241 client := s.APIState.Client() 242 reader, err := client.OpenURI("/tools/"+toolsVersion, nil) 243 c.Assert(err, jc.ErrorIsNil) 244 defer reader.Close() 245 246 // The fake tools content will be the version number. 247 content, err := ioutil.ReadAll(reader) 248 c.Assert(err, jc.ErrorIsNil) 249 c.Assert(string(content), gc.Equals, toolsVersion) 250 } 251 252 func (s *clientSuite) TestOpenURIError(c *gc.C) { 253 client := s.APIState.Client() 254 _, err := client.OpenURI("/tools/foobar", nil) 255 c.Assert(err, gc.ErrorMatches, ".+error parsing version.+") 256 } 257 258 func (s *clientSuite) TestOpenCharmFound(c *gc.C) { 259 client := s.APIState.Client() 260 curl, ch := addLocalCharm(c, client, "dummy") 261 expected, err := ioutil.ReadFile(ch.Path) 262 c.Assert(err, jc.ErrorIsNil) 263 264 reader, err := client.OpenCharm(curl) 265 defer reader.Close() 266 c.Assert(err, jc.ErrorIsNil) 267 268 data, err := ioutil.ReadAll(reader) 269 c.Assert(err, jc.ErrorIsNil) 270 c.Check(data, jc.DeepEquals, expected) 271 } 272 273 func (s *clientSuite) TestOpenCharmMissing(c *gc.C) { 274 curl := charm.MustParseURL("cs:quantal/spam-3") 275 client := s.APIState.Client() 276 277 _, err := client.OpenCharm(curl) 278 279 c.Check(err, gc.ErrorMatches, `.*cannot get charm from state: charm "cs:quantal/spam-3" not found`) 280 } 281 282 func addLocalCharm(c *gc.C, client *api.Client, name string) (*charm.URL, *charm.CharmArchive) { 283 charmArchive := testcharms.Repo.CharmArchive(c.MkDir(), name) 284 curl := charm.MustParseURL(fmt.Sprintf("local:quantal/%s-%d", charmArchive.Meta().Name, charmArchive.Revision())) 285 _, err := client.AddLocalCharm(curl, charmArchive) 286 c.Assert(err, jc.ErrorIsNil) 287 return curl, charmArchive 288 } 289 290 func fakeAPIEndpoint(c *gc.C, client *api.Client, address, method string, handle func(http.ResponseWriter, *http.Request)) net.Listener { 291 lis, err := net.Listen("tcp", "127.0.0.1:0") 292 c.Assert(err, jc.ErrorIsNil) 293 294 mux := http.NewServeMux() 295 mux.HandleFunc(address, func(w http.ResponseWriter, r *http.Request) { 296 if r.Method == method { 297 handle(w, r) 298 } 299 }) 300 go func() { 301 http.Serve(lis, mux) 302 }() 303 api.SetServerAddress(client, "http", lis.Addr().String()) 304 return lis 305 } 306 307 // envEndpoint returns "/model/<model-uuid>/<destination>" 308 func envEndpoint(c *gc.C, apiState api.Connection, destination string) string { 309 modelTag, ok := apiState.ModelTag() 310 c.Assert(ok, jc.IsTrue) 311 return path.Join("/model", modelTag.Id(), destination) 312 } 313 314 func (s *clientSuite) TestClientModelUUID(c *gc.C) { 315 model, err := s.State.Model() 316 c.Assert(err, jc.ErrorIsNil) 317 318 client := s.APIState.Client() 319 uuid, ok := client.ModelUUID() 320 c.Assert(ok, jc.IsTrue) 321 c.Assert(uuid, gc.Equals, model.Tag().Id()) 322 } 323 324 func (s *clientSuite) TestClientModelUsers(c *gc.C) { 325 client := s.APIState.Client() 326 cleanup := api.PatchClientFacadeCall(client, 327 func(request string, paramsIn interface{}, response interface{}) error { 328 c.Assert(paramsIn, gc.IsNil) 329 if response, ok := response.(*params.ModelUserInfoResults); ok { 330 response.Results = []params.ModelUserInfoResult{ 331 {Result: ¶ms.ModelUserInfo{UserName: "one"}}, 332 {Result: ¶ms.ModelUserInfo{UserName: "two"}}, 333 {Result: ¶ms.ModelUserInfo{UserName: "three"}}, 334 } 335 } else { 336 c.Log("wrong output structure") 337 c.Fail() 338 } 339 return nil 340 }, 341 ) 342 defer cleanup() 343 344 obtained, err := client.ModelUserInfo() 345 c.Assert(err, jc.ErrorIsNil) 346 347 c.Assert(obtained, jc.DeepEquals, []params.ModelUserInfo{ 348 {UserName: "one"}, 349 {UserName: "two"}, 350 {UserName: "three"}, 351 }) 352 } 353 354 func (s *clientSuite) TestWatchDebugLogConnected(c *gc.C) { 355 client := s.APIState.Client() 356 // Use the no tail option so we don't try to start a tailing cursor 357 // on the oplog when there is no oplog configured in mongo as the tests 358 // don't set up mongo in replicaset mode. 359 messages, err := client.WatchDebugLog(api.DebugLogParams{NoTail: true}) 360 c.Assert(err, jc.ErrorIsNil) 361 c.Assert(messages, gc.NotNil) 362 } 363 364 func (s *clientSuite) TestConnectStreamRequiresSlashPathPrefix(c *gc.C) { 365 reader, err := s.APIState.ConnectStream("foo", nil) 366 c.Assert(err, gc.ErrorMatches, `cannot make API path from non-slash-prefixed path "foo"`) 367 c.Assert(reader, gc.Equals, nil) 368 } 369 370 func (s *clientSuite) TestConnectStreamErrorBadConnection(c *gc.C) { 371 s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (base.Stream, error) { 372 return nil, fmt.Errorf("bad connection") 373 }) 374 reader, err := s.APIState.ConnectStream("/", nil) 375 c.Assert(err, gc.ErrorMatches, "bad connection") 376 c.Assert(reader, gc.IsNil) 377 } 378 379 func (s *clientSuite) TestConnectStreamErrorNoData(c *gc.C) { 380 s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (base.Stream, error) { 381 return fakeStreamReader{&bytes.Buffer{}}, nil 382 }) 383 reader, err := s.APIState.ConnectStream("/", nil) 384 c.Assert(err, gc.ErrorMatches, "unable to read initial response: EOF") 385 c.Assert(reader, gc.IsNil) 386 } 387 388 func (s *clientSuite) TestConnectStreamErrorBadData(c *gc.C) { 389 s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (base.Stream, error) { 390 return fakeStreamReader{strings.NewReader("junk\n")}, nil 391 }) 392 reader, err := s.APIState.ConnectStream("/", nil) 393 c.Assert(err, gc.ErrorMatches, "unable to unmarshal initial response: .*") 394 c.Assert(reader, gc.IsNil) 395 } 396 397 func (s *clientSuite) TestConnectStreamErrorReadError(c *gc.C) { 398 s.PatchValue(api.WebsocketDialConfig, func(_ *websocket.Config) (base.Stream, error) { 399 err := fmt.Errorf("bad read") 400 return fakeStreamReader{&badReader{err}}, nil 401 }) 402 reader, err := s.APIState.ConnectStream("/", nil) 403 c.Assert(err, gc.ErrorMatches, "unable to read initial response: bad read") 404 c.Assert(reader, gc.IsNil) 405 } 406 407 func (s *clientSuite) TestWatchDebugLogParamsEncoded(c *gc.C) { 408 catcher := urlCatcher{} 409 s.PatchValue(api.WebsocketDialConfig, catcher.recordLocation) 410 411 params := api.DebugLogParams{ 412 IncludeEntity: []string{"a", "b"}, 413 IncludeModule: []string{"c", "d"}, 414 ExcludeEntity: []string{"e", "f"}, 415 ExcludeModule: []string{"g", "h"}, 416 Limit: 100, 417 Backlog: 200, 418 Level: loggo.ERROR, 419 Replay: true, 420 NoTail: true, 421 } 422 423 client := s.APIState.Client() 424 _, err := client.WatchDebugLog(params) 425 c.Assert(err, jc.ErrorIsNil) 426 427 connectURL := catcher.location 428 values := connectURL.Query() 429 c.Assert(values, jc.DeepEquals, url.Values{ 430 "includeEntity": params.IncludeEntity, 431 "includeModule": params.IncludeModule, 432 "excludeEntity": params.ExcludeEntity, 433 "excludeModule": params.ExcludeModule, 434 "maxLines": {"100"}, 435 "backlog": {"200"}, 436 "level": {"ERROR"}, 437 "replay": {"true"}, 438 "noTail": {"true"}, 439 }) 440 } 441 442 func (s *clientSuite) TestConnectStreamAtUUIDPath(c *gc.C) { 443 catcher := urlCatcher{} 444 s.PatchValue(api.WebsocketDialConfig, catcher.recordLocation) 445 model, err := s.State.Model() 446 c.Assert(err, jc.ErrorIsNil) 447 info := s.APIInfo(c) 448 info.ModelTag = model.ModelTag() 449 apistate, err := api.Open(info, api.DialOpts{}) 450 c.Assert(err, jc.ErrorIsNil) 451 defer apistate.Close() 452 _, err = apistate.ConnectStream("/path", nil) 453 c.Assert(err, jc.ErrorIsNil) 454 connectURL := catcher.location 455 c.Assert(connectURL.Path, gc.Matches, fmt.Sprintf("/model/%s/path", model.UUID())) 456 } 457 458 func (s *clientSuite) TestOpenUsesModelUUIDPaths(c *gc.C) { 459 info := s.APIInfo(c) 460 461 // Passing in the correct model UUID should work 462 model, err := s.State.Model() 463 c.Assert(err, jc.ErrorIsNil) 464 info.ModelTag = model.ModelTag() 465 apistate, err := api.Open(info, api.DialOpts{}) 466 c.Assert(err, jc.ErrorIsNil) 467 apistate.Close() 468 469 // Passing in a bad model UUID should fail with a known error 470 info.ModelTag = names.NewModelTag("dead-beef-123456") 471 apistate, err = api.Open(info, api.DialOpts{}) 472 c.Assert(errors.Cause(err), gc.DeepEquals, &rpc.RequestError{ 473 Message: `unknown model: "dead-beef-123456"`, 474 Code: "model not found", 475 }) 476 c.Check(err, jc.Satisfies, params.IsCodeModelNotFound) 477 c.Assert(apistate, gc.IsNil) 478 } 479 480 func (s *clientSuite) TestSetModelAgentVersionDuringUpgrade(c *gc.C) { 481 // This is an integration test which ensure that a test with the 482 // correct error code is seen by the client from the 483 // SetModelAgentVersion call when an upgrade is in progress. 484 envConfig, err := s.State.ModelConfig() 485 c.Assert(err, jc.ErrorIsNil) 486 agentVersion, ok := envConfig.AgentVersion() 487 c.Assert(ok, jc.IsTrue) 488 machine := s.Factory.MakeMachine(c, &factory.MachineParams{ 489 Jobs: []state.MachineJob{state.JobManageModel}, 490 }) 491 err = machine.SetAgentVersion(version.MustParseBinary(agentVersion.String() + "-quantal-amd64")) 492 c.Assert(err, jc.ErrorIsNil) 493 nextVersion := version.MustParse("9.8.7") 494 _, err = s.State.EnsureUpgradeInfo(machine.Id(), agentVersion, nextVersion) 495 c.Assert(err, jc.ErrorIsNil) 496 497 err = s.APIState.Client().SetModelAgentVersion(nextVersion) 498 499 // Expect an error with a error code that indicates this specific 500 // situation. The client needs to be able to reliably identify 501 // this error and handle it differently to other errors. 502 c.Assert(params.IsCodeUpgradeInProgress(err), jc.IsTrue) 503 } 504 505 func (s *clientSuite) TestAbortCurrentUpgrade(c *gc.C) { 506 client := s.APIState.Client() 507 someErr := errors.New("random") 508 cleanup := api.PatchClientFacadeCall(client, 509 func(request string, args interface{}, response interface{}) error { 510 c.Assert(request, gc.Equals, "AbortCurrentUpgrade") 511 c.Assert(args, gc.IsNil) 512 c.Assert(response, gc.IsNil) 513 return someErr 514 }, 515 ) 516 defer cleanup() 517 518 err := client.AbortCurrentUpgrade() 519 c.Assert(err, gc.Equals, someErr) // Confirms that the correct facade was called 520 } 521 522 // badReader raises err when Read is called. 523 type badReader struct { 524 err error 525 } 526 527 func (r *badReader) Read(p []byte) (n int, err error) { 528 return 0, r.err 529 } 530 531 type urlCatcher struct { 532 location *url.URL 533 } 534 535 func (u *urlCatcher) recordLocation(config *websocket.Config) (base.Stream, error) { 536 u.location = config.Location 537 pr, pw := io.Pipe() 538 go func() { 539 fmt.Fprintf(pw, "null\n") 540 }() 541 return fakeStreamReader{pr}, nil 542 } 543 544 type fakeStreamReader struct { 545 io.Reader 546 } 547 548 func (s fakeStreamReader) Close() error { 549 if c, ok := s.Reader.(io.Closer); ok { 550 return c.Close() 551 } 552 return nil 553 } 554 555 func (s fakeStreamReader) Write([]byte) (int, error) { 556 return 0, errors.NotImplementedf("Write") 557 } 558 559 func (s fakeStreamReader) ReadJSON(v interface{}) error { 560 return errors.NotImplementedf("ReadJSON") 561 } 562 563 func (s fakeStreamReader) WriteJSON(v interface{}) error { 564 return errors.NotImplementedf("WriteJSON") 565 }