github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/api/apiclient_test.go (about) 1 // Copyright 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package api_test 5 6 import ( 7 "net" 8 "sync/atomic" 9 "time" 10 11 "github.com/juju/errors" 12 "github.com/juju/retry" 13 "github.com/juju/testing" 14 jc "github.com/juju/testing/checkers" 15 "github.com/juju/utils/clock" 16 "github.com/juju/utils/parallel" 17 "golang.org/x/net/websocket" 18 gc "gopkg.in/check.v1" 19 "gopkg.in/juju/names.v2" 20 21 "github.com/juju/juju/api" 22 "github.com/juju/juju/apiserver/params" 23 apiservertesting "github.com/juju/juju/apiserver/testing" 24 jjtesting "github.com/juju/juju/juju/testing" 25 "github.com/juju/juju/network" 26 "github.com/juju/juju/rpc" 27 jtesting "github.com/juju/juju/testing" 28 jujuversion "github.com/juju/juju/version" 29 ) 30 31 type apiclientSuite struct { 32 jjtesting.JujuConnSuite 33 } 34 35 var _ = gc.Suite(&apiclientSuite{}) 36 37 func (s *apiclientSuite) TestConnectWebsocketToEnv(c *gc.C) { 38 info := s.APIInfo(c) 39 conn, _, err := api.ConnectWebsocket(info, api.DialOpts{}) 40 c.Assert(err, jc.ErrorIsNil) 41 defer conn.Close() 42 assertConnAddrForEnv(c, conn, info.Addrs[0], s.State.ModelUUID(), "/api") 43 } 44 45 func (s *apiclientSuite) TestConnectWebsocketToRoot(c *gc.C) { 46 info := s.APIInfo(c) 47 info.ModelTag = names.NewModelTag("") 48 conn, _, err := api.ConnectWebsocket(info, api.DialOpts{}) 49 c.Assert(err, jc.ErrorIsNil) 50 defer conn.Close() 51 assertConnAddrForRoot(c, conn, info.Addrs[0]) 52 } 53 54 func (s *apiclientSuite) TestConnectWebsocketMultiple(c *gc.C) { 55 // Create a socket that proxies to the API server. 56 info := s.APIInfo(c) 57 serverAddr := info.Addrs[0] 58 proxy := testing.NewTCPProxy(c, serverAddr) 59 defer proxy.Close() 60 61 // Check that we can use the proxy to connect. 62 info.Addrs = []string{proxy.Addr()} 63 conn, _, err := api.ConnectWebsocket(info, api.DialOpts{}) 64 c.Assert(err, jc.ErrorIsNil) 65 conn.Close() 66 assertConnAddrForEnv(c, conn, proxy.Addr(), s.State.ModelUUID(), "/api") 67 68 // Now break Addrs[0], and ensure that Addrs[1] 69 // is successfully connected to. 70 proxy.Close() 71 info.Addrs = []string{proxy.Addr(), serverAddr} 72 conn, _, err = api.ConnectWebsocket(info, api.DialOpts{}) 73 c.Assert(err, jc.ErrorIsNil) 74 conn.Close() 75 assertConnAddrForEnv(c, conn, serverAddr, s.State.ModelUUID(), "/api") 76 } 77 78 func (s *apiclientSuite) TestConnectWebsocketMultipleError(c *gc.C) { 79 listener, err := net.Listen("tcp", "127.0.0.1:0") 80 c.Assert(err, jc.ErrorIsNil) 81 defer listener.Close() 82 // count holds the number of times we've accepted a connection. 83 var count int32 84 go func() { 85 for { 86 client, err := listener.Accept() 87 if err != nil { 88 return 89 } 90 atomic.AddInt32(&count, 1) 91 client.Close() 92 } 93 }() 94 info := s.APIInfo(c) 95 addr := listener.Addr().String() 96 info.Addrs = []string{addr, addr, addr} 97 _, _, err = api.ConnectWebsocket(info, api.DialOpts{}) 98 c.Assert(err, gc.ErrorMatches, `unable to connect to API: websocket.Dial wss://.*/model/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/api: .*`) 99 c.Assert(atomic.LoadInt32(&count), gc.Equals, int32(3)) 100 } 101 102 func (s *apiclientSuite) TestOpen(c *gc.C) { 103 info := s.APIInfo(c) 104 st, err := api.Open(info, api.DialOpts{}) 105 c.Assert(err, jc.ErrorIsNil) 106 defer st.Close() 107 108 c.Assert(st.Addr(), gc.Equals, info.Addrs[0]) 109 modelTag, ok := st.ModelTag() 110 c.Assert(ok, jc.IsTrue) 111 c.Assert(modelTag, gc.Equals, s.State.ModelTag()) 112 113 remoteVersion, versionSet := st.ServerVersion() 114 c.Assert(versionSet, jc.IsTrue) 115 c.Assert(remoteVersion, gc.Equals, jujuversion.Current) 116 } 117 118 func (s *apiclientSuite) TestOpenHonorsModelTag(c *gc.C) { 119 info := s.APIInfo(c) 120 121 // TODO(jam): 2014-06-05 http://pad.lv/1326802 122 // we want to test this eventually, but for now s.APIInfo uses 123 // conn.StateInfo() which doesn't know about ModelTag. 124 // c.Check(info.ModelTag, gc.Equals, env.Tag()) 125 // c.Assert(info.ModelTag, gc.Not(gc.Equals), "") 126 127 // We start by ensuring we have an invalid tag, and Open should fail. 128 info.ModelTag = names.NewModelTag("bad-tag") 129 _, err := api.Open(info, api.DialOpts{}) 130 c.Assert(errors.Cause(err), gc.DeepEquals, &rpc.RequestError{ 131 Message: `unknown model: "bad-tag"`, 132 Code: "model not found", 133 }) 134 c.Check(params.ErrCode(err), gc.Equals, params.CodeModelNotFound) 135 136 // Now set it to the right tag, and we should succeed. 137 info.ModelTag = s.State.ModelTag() 138 st, err := api.Open(info, api.DialOpts{}) 139 c.Assert(err, jc.ErrorIsNil) 140 st.Close() 141 142 // Backwards compatibility, we should succeed if we do not set an 143 // model tag 144 info.ModelTag = names.NewModelTag("") 145 st, err = api.Open(info, api.DialOpts{}) 146 c.Assert(err, jc.ErrorIsNil) 147 st.Close() 148 } 149 150 func (s *apiclientSuite) TestServerRoot(c *gc.C) { 151 url := api.ServerRoot(s.APIState.Client()) 152 c.Assert(url, gc.Matches, "https://localhost:[0-9]+") 153 } 154 155 func (s *apiclientSuite) TestDialWebsocketStopped(c *gc.C) { 156 stopped := make(chan struct{}) 157 f := api.NewWebsocketDialer(nil, api.DialOpts{}) 158 close(stopped) 159 result, err := f(stopped) 160 c.Assert(err, gc.Equals, parallel.ErrStopped) 161 c.Assert(result, gc.IsNil) 162 } 163 164 func (s *apiclientSuite) TestOpenWithNoCACert(c *gc.C) { 165 // This is hard to test as we have no way of affecting the system roots, 166 // so instead we check that the error that we get implies that 167 // we're using the system roots. 168 169 info := s.APIInfo(c) 170 info.CACert = "" 171 172 t0 := time.Now() 173 // Use a long timeout so that we can check that the retry 174 // logic doesn't retry. 175 _, err := api.Open(info, api.DialOpts{ 176 Timeout: 20 * time.Second, 177 RetryDelay: 2 * time.Second, 178 }) 179 c.Assert(err, gc.ErrorMatches, `unable to connect to API: websocket.Dial wss://.*/api: x509: certificate signed by unknown authority`) 180 181 if time.Since(t0) > 5*time.Second { 182 c.Errorf("looks like API is retrying on connection when there is an X509 error") 183 } 184 } 185 186 func (s *apiclientSuite) TestOpenWithRedirect(c *gc.C) { 187 redirectToHosts := []string{"0.1.2.3:1234", "0.1.2.4:1235"} 188 redirectToCACert := "fake CA cert" 189 190 srv := apiservertesting.NewAPIServer(func(modelUUID string) interface{} { 191 return &redirectAPI{ 192 modelUUID: modelUUID, 193 redirectToHosts: redirectToHosts, 194 redirectToCACert: redirectToCACert, 195 } 196 }) 197 defer srv.Close() 198 199 _, err := api.Open(&api.Info{ 200 Addrs: srv.Addrs, 201 CACert: jtesting.CACert, 202 ModelTag: names.NewModelTag("beef1beef1-0000-0000-000011112222"), 203 }, api.DialOpts{}) 204 c.Assert(err, gc.ErrorMatches, `redirection to alternative server required`) 205 206 hps, _ := network.ParseHostPorts(redirectToHosts...) 207 c.Assert(errors.Cause(err), jc.DeepEquals, &api.RedirectError{ 208 Servers: [][]network.HostPort{hps}, 209 CACert: redirectToCACert, 210 }) 211 } 212 213 func (s *apiclientSuite) TestAPICallNoError(c *gc.C) { 214 clock := &fakeClock{} 215 conn := api.NewTestingState(api.TestingStateParams{ 216 RPCConnection: &fakeRPCConnection{}, 217 Clock: clock, 218 }) 219 220 err := conn.APICall("facade", 1, "id", "method", nil, nil) 221 c.Check(err, jc.ErrorIsNil) 222 c.Check(clock.waits, gc.HasLen, 0) 223 } 224 225 func (s *apiclientSuite) TestAPICallError(c *gc.C) { 226 clock := &fakeClock{} 227 conn := api.NewTestingState(api.TestingStateParams{ 228 RPCConnection: &fakeRPCConnection{ 229 errors: []error{errors.BadRequestf("boom")}, 230 }, 231 Clock: clock, 232 }) 233 234 err := conn.APICall("facade", 1, "id", "method", nil, nil) 235 c.Check(err.Error(), gc.Equals, "boom") 236 c.Check(err, jc.Satisfies, errors.IsBadRequest) 237 c.Check(clock.waits, gc.HasLen, 0) 238 } 239 240 func (s *apiclientSuite) TestAPICallRetries(c *gc.C) { 241 clock := &fakeClock{} 242 conn := api.NewTestingState(api.TestingStateParams{ 243 RPCConnection: &fakeRPCConnection{ 244 errors: []error{ 245 errors.Trace( 246 &rpc.RequestError{ 247 Message: "hmm...", 248 Code: params.CodeRetry, 249 }), 250 }, 251 }, 252 Clock: clock, 253 }) 254 255 err := conn.APICall("facade", 1, "id", "method", nil, nil) 256 c.Check(err, jc.ErrorIsNil) 257 c.Check(clock.waits, jc.DeepEquals, []time.Duration{100 * time.Millisecond}) 258 } 259 260 func (s *apiclientSuite) TestAPICallRetriesLimit(c *gc.C) { 261 clock := &fakeClock{} 262 retryError := errors.Trace(&rpc.RequestError{Message: "hmm...", Code: params.CodeRetry}) 263 var errors []error 264 for i := 0; i < 10; i++ { 265 errors = append(errors, retryError) 266 } 267 conn := api.NewTestingState(api.TestingStateParams{ 268 RPCConnection: &fakeRPCConnection{ 269 errors: errors, 270 }, 271 Clock: clock, 272 }) 273 274 err := conn.APICall("facade", 1, "id", "method", nil, nil) 275 c.Check(err, jc.Satisfies, retry.IsDurationExceeded) 276 c.Check(err, gc.ErrorMatches, `.*hmm... \(retry\)`) 277 c.Check(clock.waits, jc.DeepEquals, []time.Duration{ 278 100 * time.Millisecond, 279 200 * time.Millisecond, 280 400 * time.Millisecond, 281 800 * time.Millisecond, 282 1500 * time.Millisecond, 283 1500 * time.Millisecond, 284 1500 * time.Millisecond, 285 1500 * time.Millisecond, 286 1500 * time.Millisecond, 287 }) 288 } 289 290 type fakeClock struct { 291 clock.Clock 292 293 now time.Time 294 waits []time.Duration 295 } 296 297 func (f *fakeClock) Now() time.Time { 298 if f.now.IsZero() { 299 f.now = time.Now() 300 } 301 return f.now 302 } 303 304 func (f *fakeClock) After(d time.Duration) <-chan time.Time { 305 f.waits = append(f.waits, d) 306 f.now = f.now.Add(d) 307 return time.After(0) 308 } 309 310 type fakeRPCConnection struct { 311 pos int 312 errors []error 313 } 314 315 func (f *fakeRPCConnection) Close() error { 316 return nil 317 } 318 319 func (f *fakeRPCConnection) Call(req rpc.Request, params, response interface{}) error { 320 if f.pos >= len(f.errors) { 321 return nil 322 } 323 err := f.errors[f.pos] 324 f.pos++ 325 return err 326 } 327 328 type redirectAPI struct { 329 redirected bool 330 modelUUID string 331 redirectToHosts []string 332 redirectToCACert string 333 } 334 335 func (r *redirectAPI) Admin(id string) (*redirectAPIAdmin, error) { 336 return &redirectAPIAdmin{r}, nil 337 } 338 339 type redirectAPIAdmin struct { 340 r *redirectAPI 341 } 342 343 func (a *redirectAPIAdmin) Login(req params.LoginRequest) (params.LoginResult, error) { 344 if a.r.modelUUID != "beef1beef1-0000-0000-000011112222" { 345 return params.LoginResult{}, errors.New("logged into unexpected model") 346 } 347 a.r.redirected = true 348 return params.LoginResult{}, params.Error{ 349 Message: "redirect", 350 Code: params.CodeRedirect, 351 } 352 } 353 354 func (a *redirectAPIAdmin) RedirectInfo() (params.RedirectInfoResult, error) { 355 if !a.r.redirected { 356 return params.RedirectInfoResult{}, errors.New("not redirected") 357 } 358 hps, err := network.ParseHostPorts(a.r.redirectToHosts...) 359 if err != nil { 360 panic(err) 361 } 362 return params.RedirectInfoResult{ 363 Servers: [][]params.HostPort{params.FromNetworkHostPorts(hps)}, 364 CACert: a.r.redirectToCACert, 365 }, nil 366 } 367 368 func assertConnAddrForEnv(c *gc.C, conn *websocket.Conn, addr, modelUUID, tail string) { 369 c.Assert(conn.RemoteAddr(), gc.Matches, "^wss://"+addr+"/model/"+modelUUID+tail+"$") 370 } 371 372 func assertConnAddrForRoot(c *gc.C, conn *websocket.Conn, addr string) { 373 c.Assert(conn.RemoteAddr(), gc.Matches, "^wss://"+addr+"/api$") 374 }