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  }