Merge lp:~axwalk/juju-core/client-cache-apihostports into lp:~go-bot/juju-core/trunk

Proposed by Andrew Wilkins
Status: Merged
Approved by: Andrew Wilkins
Approved revision: no longer in the source branch.
Merged at revision: 2517
Proposed branch: lp:~axwalk/juju-core/client-cache-apihostports
Merge into: lp:~go-bot/juju-core/trunk
Prerequisite: lp:~axwalk/juju-core/apihostports-api
Diff against target: 876 lines (+350/-109)
9 files modified
juju/api.go (+92/-42)
juju/apiconn_test.go (+58/-54)
juju/export_test.go (+16/-3)
juju/mock_test.go (+28/-0)
state/api/apiclient.go (+25/-2)
state/api/params/params.go (+5/-0)
state/api/state.go (+42/-1)
state/apiserver/admin.go (+13/-6)
state/apiserver/login_test.go (+71/-1)
To merge this branch: bzr merge lp:~axwalk/juju-core/client-cache-apihostports
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+213217@code.launchpad.net

Commit message

Cache API server addresses in .jenv

We now return API server addresses from
the Login API, and cache those addresses
in the client's configstore. If the client
connects using environments.yaml, then it
is expected to get updated addresses from
provider-state.

https://codereview.appspot.com/81780043/

Description of the change

Cache API server addresses in .jenv

We now return API server addresses from
the Login API, and cache those addresses
in the client's configstore. If the client
connects using environments.yaml, then it
is expected to get updated addresses from
provider-state.

https://codereview.appspot.com/81780043/

To post a comment you must log in.
Revision history for this message
Andrew Wilkins (axwalk) wrote :

Reviewers: mp+213217_code.launchpad.net,

Message:
Please take a look.

Description:
Cache API server addresses in .jenv

We now return API server addresses from
the Login API, and cache those addresses
in the client's configstore. If the client
connects using environments.yaml, then it
is expected to get updated addresses from
provider-state.

https://code.launchpad.net/~axwalk/juju-core/client-cache-apihostports/+merge/213217

Requires:
https://code.launchpad.net/~axwalk/juju-core/apihostports-api/+merge/213200

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/81780043/

Affected files (+173, -11 lines):
   A [revision details]
   M juju/api.go
   M juju/apiconn_test.go
   M state/api/apiclient.go
   M state/api/params/params.go
   M state/api/state.go
   M state/apiserver/admin.go
   M state/apiserver/login_test.go

Revision history for this message
Roger Peppe (rogpeppe) wrote :

Great direction, thanks, with a few suggestions below.

https://codereview.appspot.com/81780043/diff/1/juju/api.go
File juju/api.go (right):

https://codereview.appspot.com/81780043/diff/1/juju/api.go#newcode252
juju/api.go:252: logger.Warningf(err.Error())
This isn't right (the error might contain % characters).

logger.Warningf("Cannot cache cache API addresses: %v", err)

(and change the error returned from cacheChangedAPIAddresses
so that the message won't be repetitious)

https://codereview.appspot.com/81780043/diff/1/juju/api.go#newcode340
juju/api.go:340: endpoint.Addresses = addrs
I'd like to add a HostPorts entry to the APIEndpoint struct
(we could perhaps deprecate the Addresses field)
and store all the APIHostPorts info there.

So all the code here would deal only with instance.HostPort,
not address strings. This leaves us free to have clients
that are more intelligent about dialling in the future.

https://codereview.appspot.com/81780043/diff/1/juju/api.go#newcode356
juju/api.go:356: sort.Strings(a)
I don't think I'd sort the addresses - the order can be significant.

https://codereview.appspot.com/81780043/diff/1/state/api/apiclient.go
File state/api/apiclient.go (right):

https://codereview.appspot.com/81780043/diff/1/state/api/apiclient.go#newcode35
state/api/apiclient.go:35: addrs []string
I think it would probably be better to store the hostports exactly as
returned by APIHostPorts here, leaving it up to client code to decide
how best to use the information.

https://codereview.appspot.com/81780043/diff/1/state/api/apiclient.go#newcode206
state/api/apiclient.go:206: func (s *State) Addrs() []string {
I'd rename this HostPorts and return all
the APIHostPorts results, with the connecting
address added as an extra element if it's
not found in the result.

Also, I wonder if this logic wouldn't live more naturally
inside the Login method.

https://codereview.appspot.com/81780043/diff/1/state/api/params/params.go
File state/api/params/params.go (right):

https://codereview.appspot.com/81780043/diff/1/state/api/params/params.go#newcode645
state/api/params/params.go:645: APIHostPortsResult
I think I'd just include the field explicitly here
rather than embedding.

https://codereview.appspot.com/81780043/diff/1/state/api/state.go
File state/api/state.go (right):

https://codereview.appspot.com/81780043/diff/1/state/api/state.go#newcode35
state/api/state.go:35: // The client must attempt *all* addresses, or
else
This comment seems like it might be more helpful to put as part of the
Addrs (or HostPorts) doc comment.

https://codereview.appspot.com/81780043/diff/1/state/apiserver/admin.go
File state/apiserver/admin.go (right):

https://codereview.appspot.com/81780043/diff/1/state/apiserver/admin.go#newcode88
state/apiserver/admin.go:88: // state; failure here is non-fatal.
I think it's perfectly reasonable to make the error fatal here.
If we can't access the APIHostPorts document, something is seriously
wrong.

https://codereview.appspot.com/81780043/

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Download full text (3.7 KiB)

Please take a look.

https://codereview.appspot.com/81780043/diff/1/juju/api.go
File juju/api.go (right):

https://codereview.appspot.com/81780043/diff/1/juju/api.go#newcode252
juju/api.go:252: logger.Warningf(err.Error())
On 2014/03/28 10:02:12, rog wrote:
> This isn't right (the error might contain % characters).

> logger.Warningf("Cannot cache cache API addresses: %v", err)

> (and change the error returned from cacheChangedAPIAddresses
> so that the message won't be repetitious)

I copied that from newAPIFromStore and clearly I didn't look too close.
Fixed both.

https://codereview.appspot.com/81780043/diff/1/juju/api.go#newcode340
juju/api.go:340: endpoint.Addresses = addrs
On 2014/03/28 10:02:12, rog wrote:
> I'd like to add a HostPorts entry to the APIEndpoint struct
> (we could perhaps deprecate the Addresses field)
> and store all the APIHostPorts info there.

> So all the code here would deal only with instance.HostPort,
> not address strings. This leaves us free to have clients
> that are more intelligent about dialling in the future.

+1

https://codereview.appspot.com/81780043/diff/1/juju/api.go#newcode356
juju/api.go:356: sort.Strings(a)
On 2014/03/28 10:02:12, rog wrote:
> I don't think I'd sort the addresses - the order can be significant.

Done.

https://codereview.appspot.com/81780043/diff/1/state/api/apiclient.go
File state/api/apiclient.go (right):

https://codereview.appspot.com/81780043/diff/1/state/api/apiclient.go#newcode35
state/api/apiclient.go:35: addrs []string
On 2014/03/28 10:02:12, rog wrote:
> I think it would probably be better to store the hostports exactly as
returned
> by APIHostPorts here, leaving it up to client code to decide how best
to use the
> information.

Done.

https://codereview.appspot.com/81780043/diff/1/state/api/apiclient.go#newcode206
state/api/apiclient.go:206: func (s *State) Addrs() []string {
On 2014/03/28 10:02:12, rog wrote:
> I'd rename this HostPorts and return all
> the APIHostPorts results, with the connecting
> address added as an extra element if it's
> not found in the result.

Done. If the connecting address isn't found, it's added with
NetworkUnknown scope.

> Also, I wonder if this logic wouldn't live more naturally
> inside the Login method.

Yeah, it does, because there's a theoretical error when converting
s.addr->HostPort. Moved.

https://codereview.appspot.com/81780043/diff/1/state/api/params/params.go
File state/api/params/params.go (right):

https://codereview.appspot.com/81780043/diff/1/state/api/params/params.go#newcode645
state/api/params/params.go:645: APIHostPortsResult
On 2014/03/28 10:02:12, rog wrote:
> I think I'd just include the field explicitly here
> rather than embedding.

Done.

https://codereview.appspot.com/81780043/diff/1/state/api/state.go
File state/api/state.go (right):

https://codereview.appspot.com/81780043/diff/1/state/api/state.go#newcode35
state/api/state.go:35: // The client must attempt *all* addresses, or
else
On 2014/03/28 10:02:12, rog wrote:
> This comment seems like it might be more helpful to put as part of the
Addrs (or
> HostPorts) doc comment.

Done.

https://codereview.appspot.com/81780043/diff/1/state/apiserver/admin.go
File st...

Read more...

Revision history for this message
Roger Peppe (rogpeppe) wrote :

LGTM with the juju tests fixed so they don't rely on calling methods on
the zero value, and a few other suggestions below.

https://codereview.appspot.com/81780043/diff/20001/juju/apiconn_test.go
File juju/apiconn_test.go (right):

https://codereview.appspot.com/81780043/diff/20001/juju/apiconn_test.go#newcode160
juju/apiconn_test.go:160: // be updated (to an empty slice).
As discussed online, I'm not sure that this is the correct approach - we
shouldn't rely on calling methods on types that we've created with New.

https://codereview.appspot.com/81780043/diff/20001/state/api/apiclient.go
File state/api/apiclient.go (right):

https://codereview.appspot.com/81780043/diff/20001/state/api/apiclient.go#newcode212
state/api/apiclient.go:212: func (s *State) HostPorts()
[][]instance.HostPort {
On further reflection, I think this method should be named APIHostPorts,
mirroring the same function in State, and making it hopefully clearer
what the returned addresses are about.

https://codereview.appspot.com/81780043/diff/20001/state/api/apiclient.go#newcode213
state/api/apiclient.go:213: return append([][]instance.HostPort{},
s.hostPorts...)
If you're serious about returning a copy, then you should probably copy
all the sub-slices too.

Otherwise, just return s.hostPorts and document that the returned slice
should not be modified.

https://codereview.appspot.com/81780043/diff/20001/state/api/state.go
File state/api/state.go (right):

https://codereview.appspot.com/81780043/diff/20001/state/api/state.go#newcode40
state/api/state.go:40: if err := st.addAddrHostPort(); err != nil {
rather than have addAddrHostPort be a method that mutates st.hostPorts,
perhaps it might be clearer to have something like:

hostPorts, err := addAddress(result.Servers, st.addr)
if err != nil {
     st.Close()
     return err
}
st.hostPorts = hostPort

// addAddress appends a new server derived from the given
// address to servers if the address is not already found
// there.
func addAddress(servers [][]instance.HostPort, addr string)
([][]instance.HostPort, error)

Then addAddress is easily testable if you want to.

BTW I have mixed feelings about returning an error if we can't split the
dial address. It means that if the dial address isn't parseable for some
reason (e.g. the port is non-numeric) we'll fail the connection, even
though we actually managed to connect.

https://codereview.appspot.com/81780043/

Revision history for this message
Andrew Wilkins (axwalk) wrote :

Please take a look.

https://codereview.appspot.com/81780043/diff/20001/juju/apiconn_test.go
File juju/apiconn_test.go (right):

https://codereview.appspot.com/81780043/diff/20001/juju/apiconn_test.go#newcode160
juju/apiconn_test.go:160: // be updated (to an empty slice).
On 2014/03/31 08:37:33, rog wrote:
> As discussed online, I'm not sure that this is the correct approach -
we
> shouldn't rely on calling methods on types that we've created with
New.

Done.

https://codereview.appspot.com/81780043/diff/20001/state/api/apiclient.go
File state/api/apiclient.go (right):

https://codereview.appspot.com/81780043/diff/20001/state/api/apiclient.go#newcode212
state/api/apiclient.go:212: func (s *State) HostPorts()
[][]instance.HostPort {
On 2014/03/31 08:37:33, rog wrote:
> On further reflection, I think this method should be named
APIHostPorts,
> mirroring the same function in State, and making it hopefully clearer
what the
> returned addresses are about.

Done.

https://codereview.appspot.com/81780043/diff/20001/state/api/apiclient.go#newcode213
state/api/apiclient.go:213: return append([][]instance.HostPort{},
s.hostPorts...)
On 2014/03/31 08:37:33, rog wrote:
> If you're serious about returning a copy, then you should probably
copy all the
> sub-slices too.

> Otherwise, just return s.hostPorts and document that the returned
slice should
> not be modified.

Hah, thanks for that. Fixed.

https://codereview.appspot.com/81780043/diff/20001/state/api/state.go
File state/api/state.go (right):

https://codereview.appspot.com/81780043/diff/20001/state/api/state.go#newcode40
state/api/state.go:40: if err := st.addAddrHostPort(); err != nil {
On 2014/03/31 08:37:33, rog wrote:
> rather than have addAddrHostPort be a method that mutates
st.hostPorts, perhaps
> it might be clearer to have something like:

> hostPorts, err := addAddress(result.Servers, st.addr)
> if err != nil {
> st.Close()
> return err
> }
> st.hostPorts = hostPort

> // addAddress appends a new server derived from the given
> // address to servers if the address is not already found
> // there.
> func addAddress(servers [][]instance.HostPort, addr string)
> ([][]instance.HostPort, error)

Done.

> Then addAddress is easily testable if you want to.

> BTW I have mixed feelings about returning an error if we can't split
the dial
> address. It means that if the dial address isn't parseable for some
reason (e.g.
> the port is non-numeric) we'll fail the connection, even though we
actually
> managed to connect.

Me too, but at the moment that remains theoretical. I'd rather have it
fail spectacularly if/when we decide to have a non-numeric
/etc/services-style port, and fix it then.

https://codereview.appspot.com/81780043/

Revision history for this message
Go Bot (go-bot) wrote :
Download full text (10.4 KiB)

The attempt to merge lp:~axwalk/juju-core/client-cache-apihostports into lp:juju-core failed. Below is the output from the failed tests.

ok launchpad.net/juju-core 0.014s
ok launchpad.net/juju-core/agent 1.133s
ok launchpad.net/juju-core/agent/mongo 0.544s
ok launchpad.net/juju-core/agent/tools 0.458s
ok launchpad.net/juju-core/bzr 5.334s
ok launchpad.net/juju-core/cert 3.032s
ok launchpad.net/juju-core/charm 0.347s
? launchpad.net/juju-core/charm/hooks [no test files]
? launchpad.net/juju-core/charm/testing [no test files]
ok launchpad.net/juju-core/cloudinit 0.033s
ok launchpad.net/juju-core/cloudinit/sshinit 0.848s
ok launchpad.net/juju-core/cmd 0.183s
ok launchpad.net/juju-core/cmd/charm-admin 0.742s
? launchpad.net/juju-core/cmd/charmd [no test files]
? launchpad.net/juju-core/cmd/charmload [no test files]
ok launchpad.net/juju-core/cmd/juju 201.222s
ok launchpad.net/juju-core/cmd/jujud 61.311s
ok launchpad.net/juju-core/cmd/plugins/juju-metadata 8.095s
? launchpad.net/juju-core/cmd/plugins/juju-restore [no test files]
ok launchpad.net/juju-core/cmd/plugins/local 0.219s
? launchpad.net/juju-core/cmd/plugins/local/juju-local [no test files]
ok launchpad.net/juju-core/constraints 0.022s
ok launchpad.net/juju-core/container 0.032s
ok launchpad.net/juju-core/container/factory 0.041s
ok launchpad.net/juju-core/container/kvm 0.184s
ok launchpad.net/juju-core/container/kvm/mock 0.035s
? launchpad.net/juju-core/container/kvm/testing [no test files]
ok launchpad.net/juju-core/container/lxc 4.328s
? launchpad.net/juju-core/container/lxc/mock [no test files]
? launchpad.net/juju-core/container/lxc/testing [no test files]
? launchpad.net/juju-core/container/testing [no test files]
ok launchpad.net/juju-core/downloader 5.222s
ok launchpad.net/juju-core/environs 2.492s
ok launchpad.net/juju-core/environs/bootstrap 10.562s
ok launchpad.net/juju-core/environs/cloudinit 0.494s
ok launchpad.net/juju-core/environs/config 2.452s
ok launchpad.net/juju-core/environs/configstore 0.029s
ok launchpad.net/juju-core/environs/filestorage 0.025s
ok launchpad.net/juju-core/environs/httpstorage 0.669s
ok launchpad.net/juju-core/environs/imagemetadata 0.412s
? launchpad.net/juju-core/environs/imagemetadata/testing [no test files]
ok launchpad.net/juju-core/environs/instances 0.036s
ok launchpad.net/juju-core/environs/jujutest 0.181s
ok launchpad.net/juju-core/environs/manual 11.573s
ok launchpad.net/juju-core/environs/simplestreams 0.245s
? launchpad.net/juju-core/environs/simplestreams/testing [no test files]
ok launchpad.net/juju-core/environs/sshstorage 0.806s
ok launchpad.net/juju-core/environs/storage 0.805s
ok launchpad.net/juju-core/environs/sync 43.459s
ok launchpad.net/juju-core/environs/testing 0.116s
ok launchpad.net/juju-core/environs/tools 4.456s
? launchpad.net/juju-core/environs/tools/testing [no test files]
ok launchpad.net/juju-core/errors 0.011s
ok launchpad.net/juju-core/instance 0.018s
? launchpad.net/juju-core/instance/testing [no test files]
ok launchpad.net/juju-core/juju 18.117s
ok launchpad.net/juju-core/juju/arch 0.0...

Revision history for this message
Roger Peppe (rogpeppe) wrote :

still LGTM

https://codereview.appspot.com/81780043/diff/40001/state/api/apiclient.go
File state/api/apiclient.go (right):

https://codereview.appspot.com/81780043/diff/40001/state/api/apiclient.go#newcode215
state/api/apiclient.go:215: hostPorts[i] = append([]instance.HostPort{},
server...)
or hostPorts[i] = append(hostPorts[i], server...)
(slightly shorter)

https://codereview.appspot.com/81780043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'juju/api.go'
--- juju/api.go 2014-03-19 03:18:41 +0000
+++ juju/api.go 2014-03-31 12:32:07 +0000
@@ -14,6 +14,7 @@
14 "launchpad.net/juju-core/environs/config"14 "launchpad.net/juju-core/environs/config"
15 "launchpad.net/juju-core/environs/configstore"15 "launchpad.net/juju-core/environs/configstore"
16 "launchpad.net/juju-core/errors"16 "launchpad.net/juju-core/errors"
17 "launchpad.net/juju-core/instance"
17 "launchpad.net/juju-core/juju/osenv"18 "launchpad.net/juju-core/juju/osenv"
18 "launchpad.net/juju-core/names"19 "launchpad.net/juju-core/names"
19 "launchpad.net/juju-core/state/api"20 "launchpad.net/juju-core/state/api"
@@ -26,24 +27,25 @@
26// The following are variables so that they can be27// The following are variables so that they can be
27// changed by tests.28// changed by tests.
28var (29var (
29 apiOpen = api.Open
30 apiClose = (*api.State).Close
31 providerConnectDelay = 2 * time.Second30 providerConnectDelay = 2 * time.Second
32)31)
3332
34// apiState wraps an api.State, redefining its Close method33// apiState provides a subset of api.State's public
35// so we can abuse it for testing purposes.34// interface, defined here so it can be mocked.
36type apiState struct {35type apiState interface {
37 st *api.State36 Close() error
37 APIHostPorts() [][]instance.HostPort
38}
39
40type apiOpenFunc func(*api.Info, api.DialOpts) (apiState, error)
41
42type apiStateCachedInfo struct {
43 apiState
38 // If cachedInfo is non-nil, it indicates that the info has been44 // If cachedInfo is non-nil, it indicates that the info has been
39 // newly retrieved, and should be cached in the config store.45 // newly retrieved, and should be cached in the config store.
40 cachedInfo *api.Info46 cachedInfo *api.Info
41}47}
4248
43func (st apiState) Close() error {
44 return apiClose(st.st)
45}
46
47// APIConn holds a connection to a juju environment and its49// APIConn holds a connection to a juju environment and its
48// associated state through its API interface.50// associated state through its API interface.
49type APIConn struct {51type APIConn struct {
@@ -62,7 +64,7 @@
62 return nil, err64 return nil, err
63 }65 }
6466
65 st, err := apiOpen(info, dialOpts)67 st, err := api.Open(info, dialOpts)
66 // TODO(rog): handle errUnauthorized when the API handles passwords.68 // TODO(rog): handle errUnauthorized when the API handles passwords.
67 if err != nil {69 if err != nil {
68 return nil, err70 return nil, err
@@ -76,7 +78,7 @@
76// Close terminates the connection to the environment and releases78// Close terminates the connection to the environment and releases
77// any associated resources.79// any associated resources.
78func (c *APIConn) Close() error {80func (c *APIConn) Close() error {
79 return apiClose(c.State)81 return c.State.Close()
80}82}
8183
82// NewAPIClientFromName returns an api.Client connected to the API Server for84// NewAPIClientFromName returns an api.Client connected to the API Server for
@@ -112,12 +114,19 @@
112 if err != nil {114 if err != nil {
113 return nil, err115 return nil, err
114 }116 }
115 return newAPIFromStore(envName, store)117 apiOpen := func(info *api.Info, opts api.DialOpts) (apiState, error) {
118 return api.Open(info, opts)
119 }
120 st, err := newAPIFromStore(envName, store, apiOpen)
121 if err != nil {
122 return nil, err
123 }
124 return st.(*api.State), nil
116}125}
117126
118// newAPIFromStore implements the bulk of NewAPIClientFromName127// newAPIFromStore implements the bulk of NewAPIClientFromName
119// but is separate for testing purposes.128// but is separate for testing purposes.
120func newAPIFromStore(envName string, store configstore.Storage) (*api.State, error) {129func newAPIFromStore(envName string, store configstore.Storage, apiOpen apiOpenFunc) (apiState, error) {
121 // Try to read the default environment configuration file.130 // Try to read the default environment configuration file.
122 // If it doesn't exist, we carry on in case131 // If it doesn't exist, we carry on in case
123 // there's some environment info for that environment.132 // there's some environment info for that environment.
@@ -170,7 +179,7 @@
170 if info != nil && len(info.APIEndpoint().Addresses) > 0 {179 if info != nil && len(info.APIEndpoint().Addresses) > 0 {
171 logger.Debugf("trying cached API connection settings")180 logger.Debugf("trying cached API connection settings")
172 try.Start(func(stop <-chan struct{}) (io.Closer, error) {181 try.Start(func(stop <-chan struct{}) (io.Closer, error) {
173 return apiInfoConnect(store, info, stop)182 return apiInfoConnect(store, info, apiOpen, stop)
174 })183 })
175 // Delay the config connection until we've spent184 // Delay the config connection until we've spent
176 // some time trying to connect to the cached info.185 // some time trying to connect to the cached info.
@@ -179,7 +188,7 @@
179 logger.Debugf("no cached API connection settings found")188 logger.Debugf("no cached API connection settings found")
180 }189 }
181 try.Start(func(stop <-chan struct{}) (io.Closer, error) {190 try.Start(func(stop <-chan struct{}) (io.Closer, error) {
182 return apiConfigConnect(info, envs, envName, stop, delay)191 return apiConfigConnect(info, envs, envName, apiOpen, stop, delay)
183 })192 })
184 try.Close()193 try.Close()
185 val0, err := try.Result()194 val0, err := try.Result()
@@ -190,20 +199,23 @@
190 }199 }
191 return nil, err200 return nil, err
192 }201 }
202
193 val := val0.(apiState)203 val := val0.(apiState)
194204 if cachedInfo, ok := val.(apiStateCachedInfo); ok {
195 if val.cachedInfo != nil && info != nil {205 val = cachedInfo.apiState
196 // Cache the connection settings only if we used the206 if cachedInfo.cachedInfo != nil && info != nil {
197 // environment config, but any errors are just logged207 // Cache the connection settings only if we used the
198 // as warnings, because they're not fatal.208 // environment config, but any errors are just logged
199 err = cacheAPIInfo(info, val.cachedInfo)209 // as warnings, because they're not fatal.
200 if err != nil {210 err = cacheAPIInfo(info, cachedInfo.cachedInfo)
201 logger.Warningf(err.Error())211 if err != nil {
202 } else {212 logger.Warningf("cannot cache API connection settings: %v", err.Error())
203 logger.Debugf("updated API connection settings cache")213 } else {
214 logger.Infof("updated API connection settings cache")
215 }
204 }216 }
205 }217 }
206 return val.st, nil218 return val, nil
207}219}
208220
209func errorImportance(err error) int {221func errorImportance(err error) int {
@@ -230,10 +242,10 @@
230242
231// apiInfoConnect looks for endpoint on the given environment and243// apiInfoConnect looks for endpoint on the given environment and
232// tries to connect to it, sending the result on the returned channel.244// tries to connect to it, sending the result on the returned channel.
233func apiInfoConnect(store configstore.Storage, info configstore.EnvironInfo, stop <-chan struct{}) (apiState, error) {245func apiInfoConnect(store configstore.Storage, info configstore.EnvironInfo, apiOpen apiOpenFunc, stop <-chan struct{}) (apiState, error) {
234 endpoint := info.APIEndpoint()246 endpoint := info.APIEndpoint()
235 if info == nil || len(endpoint.Addresses) == 0 {247 if info == nil || len(endpoint.Addresses) == 0 {
236 return apiState{}, &infoConnectError{fmt.Errorf("no cached addresses")}248 return nil, &infoConnectError{fmt.Errorf("no cached addresses")}
237 }249 }
238 logger.Infof("connecting to API addresses: %v", endpoint.Addresses)250 logger.Infof("connecting to API addresses: %v", endpoint.Addresses)
239 apiInfo := &api.Info{251 apiInfo := &api.Info{
@@ -244,9 +256,19 @@
244 }256 }
245 st, err := apiOpen(apiInfo, api.DefaultDialOpts())257 st, err := apiOpen(apiInfo, api.DefaultDialOpts())
246 if err != nil {258 if err != nil {
247 return apiState{}, &infoConnectError{err}259 return nil, &infoConnectError{err}
248 }260 }
249 return apiState{st, nil}, err261 var addrs []string
262 for _, serverHostPorts := range st.APIHostPorts() {
263 for _, hostPort := range serverHostPorts {
264 addrs = append(addrs, hostPort.NetAddr())
265 }
266 }
267 // Update API addresses if they've changed. Error is non-fatal.
268 if err := cacheChangedAPIAddresses(info, addrs); err != nil {
269 logger.Warningf("cannot cache API addresses: %v", err.Error())
270 }
271 return st, err
250}272}
251273
252// apiConfigConnect looks for configuration info on the given environment,274// apiConfigConnect looks for configuration info on the given environment,
@@ -254,7 +276,7 @@
254// its endpoint. It only starts the attempt after the given delay,276// its endpoint. It only starts the attempt after the given delay,
255// to allow the faster apiInfoConnect to hopefully succeed first.277// to allow the faster apiInfoConnect to hopefully succeed first.
256// It returns nil if there was no configuration information found.278// It returns nil if there was no configuration information found.
257func apiConfigConnect(info configstore.EnvironInfo, envs *environs.Environs, envName string, stop <-chan struct{}, delay time.Duration) (apiState, error) {279func apiConfigConnect(info configstore.EnvironInfo, envs *environs.Environs, envName string, apiOpen apiOpenFunc, stop <-chan struct{}, delay time.Duration) (apiState, error) {
258 var cfg *config.Config280 var cfg *config.Config
259 var err error281 var err error
260 if info != nil && len(info.BootstrapConfig()) > 0 {282 if info != nil && len(info.BootstrapConfig()) > 0 {
@@ -262,30 +284,30 @@
262 } else if envs != nil {284 } else if envs != nil {
263 cfg, err = envs.Config(envName)285 cfg, err = envs.Config(envName)
264 if errors.IsNotFoundError(err) {286 if errors.IsNotFoundError(err) {
265 return apiState{}, err287 return nil, err
266 }288 }
267 } else {289 } else {
268 return apiState{}, errors.NotFoundf("environment %q", envName)290 return nil, errors.NotFoundf("environment %q", envName)
269 }291 }
270 select {292 select {
271 case <-time.After(delay):293 case <-time.After(delay):
272 case <-stop:294 case <-stop:
273 return apiState{}, errAborted295 return nil, errAborted
274 }296 }
275 environ, err := environs.New(cfg)297 environ, err := environs.New(cfg)
276 if err != nil {298 if err != nil {
277 return apiState{}, err299 return nil, err
278 }300 }
279 apiInfo, err := environAPIInfo(environ)301 apiInfo, err := environAPIInfo(environ)
280 if err != nil {302 if err != nil {
281 return apiState{}, err303 return nil, err
282 }304 }
283 st, err := apiOpen(apiInfo, api.DefaultDialOpts())305 st, err := apiOpen(apiInfo, api.DefaultDialOpts())
284 // TODO(rog): handle errUnauthorized when the API handles passwords.306 // TODO(rog): handle errUnauthorized when the API handles passwords.
285 if err != nil {307 if err != nil {
286 return apiState{}, err308 return nil, err
287 }309 }
288 return apiState{st, apiInfo}, nil310 return apiStateCachedInfo{st, apiInfo}, nil
289}311}
290312
291func environAPIInfo(environ environs.Environ) (*api.Info, error) {313func environAPIInfo(environ environs.Environ) (*api.Info, error) {
@@ -312,14 +334,42 @@
312 })334 })
313 _, username, err := names.ParseTag(apiInfo.Tag, names.UserTagKind)335 _, username, err := names.ParseTag(apiInfo.Tag, names.UserTagKind)
314 if err != nil {336 if err != nil {
315 return fmt.Errorf("not caching API connection settings: invalid API user tag: %v", err)337 return fmt.Errorf("invalid API user tag: %v", err)
316 }338 }
317 info.SetAPICredentials(configstore.APICredentials{339 info.SetAPICredentials(configstore.APICredentials{
318 User: username,340 User: username,
319 Password: apiInfo.Password,341 Password: apiInfo.Password,
320 })342 })
343 return info.Write()
344}
345
346// cacheChangedAPIAddresses updates the local environment settings (.jenv file)
347// with the provided API server addresses if they have changed.
348func cacheChangedAPIAddresses(info configstore.EnvironInfo, addrs []string) error {
349 endpoint := info.APIEndpoint()
350 if !addrsChanged(endpoint.Addresses, addrs) {
351 return nil
352 }
353 logger.Debugf("API addresses changed from %q to %q", endpoint.Addresses, addrs)
354 endpoint.Addresses = addrs
355 info.SetAPIEndpoint(endpoint)
321 if err := info.Write(); err != nil {356 if err := info.Write(); err != nil {
322 return fmt.Errorf("cannot cache API connection settings: %v", err)357 return err
323 }358 }
359 logger.Infof("updated API connection settings cache")
324 return nil360 return nil
325}361}
362
363// addrsChanged returns true iff the two
364// slices are not equal. Order is important.
365func addrsChanged(a, b []string) bool {
366 if len(a) != len(b) {
367 return true
368 }
369 for i := range a {
370 if a[i] != b[i] {
371 return true
372 }
373 }
374 return false
375}
326376
=== modified file 'juju/apiconn_test.go'
--- juju/apiconn_test.go 2014-03-19 22:16:15 +0000
+++ juju/apiconn_test.go 2014-03-31 12:32:07 +0000
@@ -8,7 +8,6 @@
8 "os"8 "os"
9 "time"9 "time"
1010
11 "github.com/juju/testing"
12 jc "github.com/juju/testing/checkers"11 jc "github.com/juju/testing/checkers"
13 gc "launchpad.net/gocheck"12 gc "launchpad.net/gocheck"
1413
@@ -18,6 +17,7 @@
18 "launchpad.net/juju-core/environs/config"17 "launchpad.net/juju-core/environs/config"
19 "launchpad.net/juju-core/environs/configstore"18 "launchpad.net/juju-core/environs/configstore"
20 envtesting "launchpad.net/juju-core/environs/testing"19 envtesting "launchpad.net/juju-core/environs/testing"
20 "launchpad.net/juju-core/instance"
21 "launchpad.net/juju-core/juju"21 "launchpad.net/juju-core/juju"
22 "launchpad.net/juju-core/juju/osenv"22 "launchpad.net/juju-core/juju/osenv"
23 "launchpad.net/juju-core/provider/dummy"23 "launchpad.net/juju-core/provider/dummy"
@@ -131,8 +131,12 @@
131 store := newConfigStore("noconfig", storeConfig)131 store := newConfigStore("noconfig", storeConfig)
132132
133 called := 0133 called := 0
134 expectState := new(api.State)134 expectState := &mockAPIState{
135 apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (*api.State, error) {135 apiHostPorts: [][]instance.HostPort{
136 instance.AddressesWithPort([]instance.Address{instance.NewAddress("0.1.2.3")}, 1234),
137 },
138 }
139 apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) {
136 c.Check(apiInfo.Tag, gc.Equals, "user-foo")140 c.Check(apiInfo.Tag, gc.Equals, "user-foo")
137 c.Check(string(apiInfo.CACert), gc.Equals, "certificated")141 c.Check(string(apiInfo.CACert), gc.Equals, "certificated")
138 c.Check(apiInfo.Password, gc.Equals, "foopass")142 c.Check(apiInfo.Password, gc.Equals, "foopass")
@@ -140,14 +144,25 @@
140 called++144 called++
141 return expectState, nil145 return expectState, nil
142 }146 }
147
143 // Give NewAPIFromStore a store interface that can report when the148 // Give NewAPIFromStore a store interface that can report when the
144 // config was written to, to ensure the cache isn't updated.149 // config was written to, to check if the cache is updated.
145 s.PatchValue(juju.APIOpen, apiOpen)
146 mockStore := &storageWithWriteNotify{store: store}150 mockStore := &storageWithWriteNotify{store: store}
147 st, err := juju.NewAPIFromStore("noconfig", mockStore)151 st, err := juju.NewAPIFromStore("noconfig", mockStore, apiOpen)
148 c.Assert(err, gc.IsNil)152 c.Assert(err, gc.IsNil)
149 c.Assert(st, gc.Equals, expectState)153 c.Assert(st, gc.Equals, expectState)
150 c.Assert(called, gc.Equals, 1)154 c.Assert(called, gc.Equals, 1)
155 c.Assert(mockStore.written, jc.IsTrue)
156 info, err := store.ReadInfo("noconfig")
157 c.Assert(err, gc.IsNil)
158 c.Assert(info.APIEndpoint().Addresses, gc.DeepEquals, []string{"0.1.2.3:1234"})
159 mockStore.written = false
160
161 // If APIHostPorts haven't changed, then the store won't be updated.
162 st, err = juju.NewAPIFromStore("noconfig", mockStore, apiOpen)
163 c.Assert(err, gc.IsNil)
164 c.Assert(st, gc.Equals, expectState)
165 c.Assert(called, gc.Equals, 2)
151 c.Assert(mockStore.written, jc.IsFalse)166 c.Assert(mockStore.written, jc.IsFalse)
152}167}
153168
@@ -177,8 +192,8 @@
177 c.Assert(info.APICredentials(), jc.DeepEquals, configstore.APICredentials{})192 c.Assert(info.APICredentials(), jc.DeepEquals, configstore.APICredentials{})
178193
179 called := 0194 called := 0
180 expectState := new(api.State)195 expectState := &mockAPIState{}
181 apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (*api.State, error) {196 apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) {
182 c.Check(apiInfo.Tag, gc.Equals, "user-admin")197 c.Check(apiInfo.Tag, gc.Equals, "user-admin")
183 c.Check(string(apiInfo.CACert), gc.Not(gc.Equals), "")198 c.Check(string(apiInfo.CACert), gc.Not(gc.Equals), "")
184 c.Check(apiInfo.Password, gc.Equals, "adminpass")199 c.Check(apiInfo.Password, gc.Equals, "adminpass")
@@ -186,8 +201,7 @@
186 called++201 called++
187 return expectState, nil202 return expectState, nil
188 }203 }
189 s.PatchValue(juju.APIOpen, apiOpen)204 st, err := juju.NewAPIFromStore("myenv", store, apiOpen)
190 st, err := juju.NewAPIFromStore("myenv", store)
191 c.Assert(err, gc.IsNil)205 c.Assert(err, gc.IsNil)
192 c.Assert(st, gc.Equals, expectState)206 c.Assert(st, gc.Equals, expectState)
193 c.Assert(called, gc.Equals, 1)207 c.Assert(called, gc.Equals, 1)
@@ -209,16 +223,11 @@
209 defer coretesting.MakeEmptyFakeHome(c).Restore()223 defer coretesting.MakeEmptyFakeHome(c).Restore()
210 expectErr := fmt.Errorf("an error")224 expectErr := fmt.Errorf("an error")
211 store := newConfigStoreWithError(expectErr)225 store := newConfigStoreWithError(expectErr)
212 s.PatchValue(juju.APIOpen, panicAPIOpen)226 client, err := juju.NewAPIFromStore("noconfig", store, panicAPIOpen)
213 client, err := juju.NewAPIFromStore("noconfig", store)
214 c.Assert(err, gc.Equals, expectErr)227 c.Assert(err, gc.Equals, expectErr)
215 c.Assert(client, gc.IsNil)228 c.Assert(client, gc.IsNil)
216}229}
217230
218func panicAPIOpen(apiInfo *api.Info, opts api.DialOpts) (*api.State, error) {
219 panic("api.Open called unexpectedly")
220}
221
222func (s *NewAPIClientSuite) TestWithInfoNoAddresses(c *gc.C) {231func (s *NewAPIClientSuite) TestWithInfoNoAddresses(c *gc.C) {
223 defer coretesting.MakeEmptyFakeHome(c).Restore()232 defer coretesting.MakeEmptyFakeHome(c).Restore()
224 store := newConfigStore("noconfig", &environInfo{233 store := newConfigStore("noconfig", &environInfo{
@@ -227,9 +236,7 @@
227 CACert: "certificated",236 CACert: "certificated",
228 },237 },
229 })238 })
230 s.PatchValue(juju.APIOpen, panicAPIOpen)239 st, err := juju.NewAPIFromStore("noconfig", store, panicAPIOpen)
231
232 st, err := juju.NewAPIFromStore("noconfig", store)
233 c.Assert(err, gc.ErrorMatches, `environment "noconfig" not found`)240 c.Assert(err, gc.ErrorMatches, `environment "noconfig" not found`)
234 c.Assert(st, gc.IsNil)241 c.Assert(st, gc.IsNil)
235}242}
@@ -243,11 +250,10 @@
243 })250 })
244251
245 expectErr := fmt.Errorf("an error")252 expectErr := fmt.Errorf("an error")
246 apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (*api.State, error) {253 apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) {
247 return nil, expectErr254 return nil, expectErr
248 }255 }
249 s.PatchValue(juju.APIOpen, apiOpen)256 st, err := juju.NewAPIFromStore("noconfig", store, apiOpen)
250 st, err := juju.NewAPIFromStore("noconfig", store)
251 c.Assert(err, gc.Equals, expectErr)257 c.Assert(err, gc.Equals, expectErr)
252 c.Assert(st, gc.IsNil)258 c.Assert(st, gc.IsNil)
253}259}
@@ -258,27 +264,30 @@
258 bootstrapEnv(c, coretesting.SampleEnvName, store)264 bootstrapEnv(c, coretesting.SampleEnvName, store)
259 setEndpointAddress(c, store, coretesting.SampleEnvName, "infoapi.invalid")265 setEndpointAddress(c, store, coretesting.SampleEnvName, "infoapi.invalid")
260266
261 infoOpenedState := new(api.State)267 infoOpenedState := &mockAPIState{}
262 infoEndpointOpened := make(chan struct{})268 infoEndpointOpened := make(chan struct{})
263 cfgOpenedState := new(api.State)269 cfgOpenedState := &mockAPIState{}
264 // On a sample run with no delay, the logic took 45ms to run, so270 // On a sample run with no delay, the logic took 45ms to run, so
265 // we make the delay slightly more than that, so that if the271 // we make the delay slightly more than that, so that if the
266 // logic doesn't delay at all, the test will fail reasonably consistently.272 // logic doesn't delay at all, the test will fail reasonably consistently.
267 s.PatchValue(juju.ProviderConnectDelay, 50*time.Millisecond)273 s.PatchValue(juju.ProviderConnectDelay, 50*time.Millisecond)
268 apiOpen := func(info *api.Info, opts api.DialOpts) (*api.State, error) {274 apiOpen := func(info *api.Info, opts api.DialOpts) (juju.APIState, error) {
269 if info.Addrs[0] == "infoapi.invalid" {275 if info.Addrs[0] == "infoapi.invalid" {
270 infoEndpointOpened <- struct{}{}276 infoEndpointOpened <- struct{}{}
271 return infoOpenedState, nil277 return infoOpenedState, nil
272 }278 }
273 return cfgOpenedState, nil279 return cfgOpenedState, nil
274 }280 }
275 s.PatchValue(juju.APIOpen, apiOpen)
276281
277 stateClosed, restoreAPIClose := setAPIClosed()282 stateClosed := make(chan juju.APIState)
278 defer restoreAPIClose.Restore()283 infoOpenedState.close = func(st juju.APIState) error {
284 stateClosed <- st
285 return nil
286 }
287 cfgOpenedState.close = infoOpenedState.close
279288
280 startTime := time.Now()289 startTime := time.Now()
281 st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store)290 st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store, apiOpen)
282 c.Assert(err, gc.IsNil)291 c.Assert(err, gc.IsNil)
283 // The connection logic should wait for some time before opening292 // The connection logic should wait for some time before opening
284 // the API from the configuration.293 // the API from the configuration.
@@ -320,13 +329,13 @@
320 bootstrapEnv(c, coretesting.SampleEnvName, store)329 bootstrapEnv(c, coretesting.SampleEnvName, store)
321 setEndpointAddress(c, store, coretesting.SampleEnvName, "infoapi.invalid")330 setEndpointAddress(c, store, coretesting.SampleEnvName, "infoapi.invalid")
322331
323 infoOpenedState := new(api.State)332 infoOpenedState := &mockAPIState{}
324 infoEndpointOpened := make(chan struct{})333 infoEndpointOpened := make(chan struct{})
325 cfgOpenedState := new(api.State)334 cfgOpenedState := &mockAPIState{}
326 cfgEndpointOpened := make(chan struct{})335 cfgEndpointOpened := make(chan struct{})
327336
328 s.PatchValue(juju.ProviderConnectDelay, 0*time.Second)337 s.PatchValue(juju.ProviderConnectDelay, 0*time.Second)
329 apiOpen := func(info *api.Info, opts api.DialOpts) (*api.State, error) {338 apiOpen := func(info *api.Info, opts api.DialOpts) (juju.APIState, error) {
330 if info.Addrs[0] == "infoapi.invalid" {339 if info.Addrs[0] == "infoapi.invalid" {
331 infoEndpointOpened <- struct{}{}340 infoEndpointOpened <- struct{}{}
332 <-infoEndpointOpened341 <-infoEndpointOpened
@@ -336,14 +345,17 @@
336 <-cfgEndpointOpened345 <-cfgEndpointOpened
337 return cfgOpenedState, nil346 return cfgOpenedState, nil
338 }347 }
339 s.PatchValue(juju.APIOpen, apiOpen)
340348
341 stateClosed, restoreAPIClose := setAPIClosed()349 stateClosed := make(chan juju.APIState)
342 defer restoreAPIClose.Restore()350 infoOpenedState.close = func(st juju.APIState) error {
351 stateClosed <- st
352 return nil
353 }
354 cfgOpenedState.close = infoOpenedState.close
343355
344 done := make(chan struct{})356 done := make(chan struct{})
345 go func() {357 go func() {
346 st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store)358 st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store, apiOpen)
347 c.Check(err, gc.IsNil)359 c.Check(err, gc.IsNil)
348 c.Check(st, gc.Equals, infoOpenedState)360 c.Check(st, gc.Equals, infoOpenedState)
349 close(done)361 close(done)
@@ -387,14 +399,13 @@
387 setEndpointAddress(c, store, coretesting.SampleEnvName, "infoapi.invalid")399 setEndpointAddress(c, store, coretesting.SampleEnvName, "infoapi.invalid")
388400
389 s.PatchValue(juju.ProviderConnectDelay, 0*time.Second)401 s.PatchValue(juju.ProviderConnectDelay, 0*time.Second)
390 apiOpen := func(info *api.Info, opts api.DialOpts) (*api.State, error) {402 apiOpen := func(info *api.Info, opts api.DialOpts) (juju.APIState, error) {
391 if info.Addrs[0] == "infoapi.invalid" {403 if info.Addrs[0] == "infoapi.invalid" {
392 return nil, fmt.Errorf("info connect failed")404 return nil, fmt.Errorf("info connect failed")
393 }405 }
394 return nil, fmt.Errorf("config connect failed")406 return nil, fmt.Errorf("config connect failed")
395 }407 }
396 s.PatchValue(juju.APIOpen, apiOpen)408 st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store, apiOpen)
397 st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store)
398 c.Check(err, gc.ErrorMatches, "config connect failed")409 c.Check(err, gc.ErrorMatches, "config connect failed")
399 c.Check(st, gc.IsNil)410 c.Check(st, gc.IsNil)
400}411}
@@ -427,7 +438,10 @@
427 err = os.Remove(osenv.JujuHomePath("environments.yaml"))438 err = os.Remove(osenv.JujuHomePath("environments.yaml"))
428 c.Assert(err, gc.IsNil)439 c.Assert(err, gc.IsNil)
429440
430 st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store)441 apiOpen := func(*api.Info, api.DialOpts) (juju.APIState, error) {
442 return &mockAPIState{}, nil
443 }
444 st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store, apiOpen)
431 c.Check(err, gc.IsNil)445 c.Check(err, gc.IsNil)
432 st.Close()446 st.Close()
433}447}
@@ -455,7 +469,10 @@
455 // Now we have info for envName2 which will actually469 // Now we have info for envName2 which will actually
456 // cause a connection to the originally bootstrapped470 // cause a connection to the originally bootstrapped
457 // state.471 // state.
458 st, err := juju.NewAPIFromStore(envName2, store)472 apiOpen := func(*api.Info, api.DialOpts) (juju.APIState, error) {
473 return &mockAPIState{}, nil
474 }
475 st, err := juju.NewAPIFromStore(envName2, store, apiOpen)
459 c.Check(err, gc.IsNil)476 c.Check(err, gc.IsNil)
460 st.Close()477 st.Close()
461478
@@ -478,19 +495,6 @@
478 c.Assert(envInfo.Name, gc.Equals, expectName)495 c.Assert(envInfo.Name, gc.Equals, expectName)
479}496}
480497
481func setAPIClosed() (<-chan *api.State, testing.Restorer) {
482 stateClosed := make(chan *api.State)
483 apiClose := func(st *api.State) error {
484 stateClosed <- st
485 return nil
486 }
487 return stateClosed, testing.PatchValue(juju.APIClose, apiClose)
488}
489
490func updateSecretsNoop(_ environs.Environ, _ *api.State) error {
491 return nil
492}
493
494// newConfigStoreWithError that will return the given498// newConfigStoreWithError that will return the given
495// error from ReadInfo.499// error from ReadInfo.
496func newConfigStoreWithError(err error) configstore.Storage {500func newConfigStoreWithError(err error) configstore.Storage {
497501
=== modified file 'juju/export_test.go'
--- juju/export_test.go 2014-03-19 03:18:41 +0000
+++ juju/export_test.go 2014-03-31 12:32:07 +0000
@@ -1,8 +1,21 @@
1package juju1package juju
22
3import (
4 "launchpad.net/juju-core/environs/configstore"
5 "launchpad.net/juju-core/state/api"
6)
7
3var (8var (
4 APIOpen = &apiOpen
5 APIClose = &apiClose
6 ProviderConnectDelay = &providerConnectDelay9 ProviderConnectDelay = &providerConnectDelay
7 NewAPIFromStore = newAPIFromStore
8)10)
11
12type APIState apiState
13
14type APIOpenFunc func(*api.Info, api.DialOpts) (APIState, error)
15
16func NewAPIFromStore(envName string, store configstore.Storage, f APIOpenFunc) (APIState, error) {
17 apiOpen := func(info *api.Info, opts api.DialOpts) (apiState, error) {
18 return f(info, opts)
19 }
20 return newAPIFromStore(envName, store, apiOpen)
21}
922
=== added file 'juju/mock_test.go'
--- juju/mock_test.go 1970-01-01 00:00:00 +0000
+++ juju/mock_test.go 2014-03-31 12:32:07 +0000
@@ -0,0 +1,28 @@
1package juju_test
2
3import (
4 "launchpad.net/juju-core/instance"
5 "launchpad.net/juju-core/juju"
6 "launchpad.net/juju-core/state/api"
7)
8
9type mockAPIState struct {
10 close func(juju.APIState) error
11
12 apiHostPorts [][]instance.HostPort
13}
14
15func (s *mockAPIState) Close() error {
16 if s.close != nil {
17 return s.close(s)
18 }
19 return nil
20}
21
22func (s *mockAPIState) APIHostPorts() [][]instance.HostPort {
23 return s.apiHostPorts
24}
25
26func panicAPIOpen(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) {
27 panic("api.Open called unexpectedly")
28}
029
=== modified file 'state/api/apiclient.go'
--- state/api/apiclient.go 2014-03-19 03:18:41 +0000
+++ state/api/apiclient.go 2014-03-31 12:32:07 +0000
@@ -11,6 +11,7 @@
11 "code.google.com/p/go.net/websocket"11 "code.google.com/p/go.net/websocket"
1212
13 "launchpad.net/juju-core/cert"13 "launchpad.net/juju-core/cert"
14 "launchpad.net/juju-core/instance"
14 "launchpad.net/juju-core/log"15 "launchpad.net/juju-core/log"
15 "launchpad.net/juju-core/rpc"16 "launchpad.net/juju-core/rpc"
16 "launchpad.net/juju-core/rpc/jsoncodec"17 "launchpad.net/juju-core/rpc/jsoncodec"
@@ -25,7 +26,13 @@
25type State struct {26type State struct {
26 client *rpc.Conn27 client *rpc.Conn
27 conn *websocket.Conn28 conn *websocket.Conn
28 addr string29
30 // addr is the address used to connect to the API server.
31 addr string
32
33 // hostPorts is the API server addresses returned from Login,
34 // which the client may cache and use for failover.
35 hostPorts [][]instance.HostPort
2936
30 // authTag holds the authenticated entity's tag after login.37 // authTag holds the authenticated entity's tag after login.
31 authTag string38 authTag string
@@ -189,7 +196,23 @@
189 return s.client196 return s.client
190}197}
191198
192// Addr returns the address used to connect to the RPC server.199// Addr returns the address used to connect to the API server.
193func (s *State) Addr() string {200func (s *State) Addr() string {
194 return s.addr201 return s.addr
195}202}
203
204// APIHostPorts returns addresses that may be used to connect
205// to the API server, including the address used to connect.
206//
207// The addresses are scoped (public, cloud-internal, etc.), so
208// the client may choose which addresses to attempt. For the
209// Juju CLI, all addresses must be attempted, as the CLI may
210// be invoked both within and outside the environment (think
211// private clouds).
212func (s *State) APIHostPorts() [][]instance.HostPort {
213 hostPorts := make([][]instance.HostPort, len(s.hostPorts))
214 for i, server := range s.hostPorts {
215 hostPorts[i] = append([]instance.HostPort{}, server...)
216 }
217 return hostPorts
218}
196219
=== modified file 'state/api/params/params.go'
--- state/api/params/params.go 2014-03-28 13:31:26 +0000
+++ state/api/params/params.go 2014-03-31 12:32:07 +0000
@@ -640,6 +640,11 @@
640 Servers [][]instance.HostPort640 Servers [][]instance.HostPort
641}641}
642642
643// LoginResult holds the result of a Login call.
644type LoginResult struct {
645 Servers [][]instance.HostPort
646}
647
643// EnsureAvailability contains arguments for648// EnsureAvailability contains arguments for
644// the EnsureAvailability client API call.649// the EnsureAvailability client API call.
645type EnsureAvailability struct {650type EnsureAvailability struct {
646651
=== modified file 'state/api/state.go'
--- state/api/state.go 2014-02-27 05:24:43 +0000
+++ state/api/state.go 2014-03-31 12:32:07 +0000
@@ -4,6 +4,10 @@
4package api4package api
55
6import (6import (
7 "net"
8 "strconv"
9
10 "launchpad.net/juju-core/instance"
7 "launchpad.net/juju-core/state/api/agent"11 "launchpad.net/juju-core/state/api/agent"
8 "launchpad.net/juju-core/state/api/charmrevisionupdater"12 "launchpad.net/juju-core/state/api/charmrevisionupdater"
9 "launchpad.net/juju-core/state/api/deployer"13 "launchpad.net/juju-core/state/api/deployer"
@@ -24,17 +28,54 @@
24// method is usually called automatically by Open. The machine nonce28// method is usually called automatically by Open. The machine nonce
25// should be empty unless logging in as a machine agent.29// should be empty unless logging in as a machine agent.
26func (st *State) Login(tag, password, nonce string) error {30func (st *State) Login(tag, password, nonce string) error {
31 var result params.LoginResult
27 err := st.Call("Admin", "", "Login", &params.Creds{32 err := st.Call("Admin", "", "Login", &params.Creds{
28 AuthTag: tag,33 AuthTag: tag,
29 Password: password,34 Password: password,
30 Nonce: nonce,35 Nonce: nonce,
31 }, nil)36 }, &result)
32 if err == nil {37 if err == nil {
33 st.authTag = tag38 st.authTag = tag
39 hostPorts, err := addAddress(result.Servers, st.addr)
40 if err != nil {
41 st.Close()
42 return err
43 }
44 st.hostPorts = hostPorts
34 }45 }
35 return err46 return err
36}47}
3748
49// addAddress appends a new server derived from the given
50// address to servers if the address is not already found
51// there.
52func addAddress(servers [][]instance.HostPort, addr string) ([][]instance.HostPort, error) {
53 for _, server := range servers {
54 for _, hostPort := range server {
55 if hostPort.NetAddr() == addr {
56 return servers, nil
57 }
58 }
59 }
60 host, portString, err := net.SplitHostPort(addr)
61 if err != nil {
62 return nil, err
63 }
64 port, err := strconv.Atoi(portString)
65 if err != nil {
66 return nil, err
67 }
68 hostPort := instance.HostPort{
69 Address: instance.Address{
70 Value: host,
71 Type: instance.DeriveAddressType(host),
72 NetworkScope: instance.NetworkUnknown,
73 },
74 Port: port,
75 }
76 return append(servers, []instance.HostPort{hostPort}), nil
77}
78
38// Client returns an object that can be used79// Client returns an object that can be used
39// to access client-specific functionality.80// to access client-specific functionality.
40func (st *State) Client() *Client {81func (st *State) Client() *Client {
4182
=== modified file 'state/apiserver/admin.go'
--- state/apiserver/admin.go 2014-01-17 19:33:42 +0000
+++ state/apiserver/admin.go 2014-03-31 12:32:07 +0000
@@ -63,16 +63,16 @@
63// Login logs in with the provided credentials.63// Login logs in with the provided credentials.
64// All subsequent requests on the connection will64// All subsequent requests on the connection will
65// act as the authenticated user.65// act as the authenticated user.
66func (a *srvAdmin) Login(c params.Creds) error {66func (a *srvAdmin) Login(c params.Creds) (params.LoginResult, error) {
67 a.mu.Lock()67 a.mu.Lock()
68 defer a.mu.Unlock()68 defer a.mu.Unlock()
69 if a.loggedIn {69 if a.loggedIn {
70 // This can only happen if Login is called concurrently.70 // This can only happen if Login is called concurrently.
71 return errAlreadyLoggedIn71 return params.LoginResult{}, errAlreadyLoggedIn
72 }72 }
73 entity, err := checkCreds(a.root.srv.state, c)73 entity, err := checkCreds(a.root.srv.state, c)
74 if err != nil {74 if err != nil {
75 return err75 return params.LoginResult{}, err
76 }76 }
77 if a.reqNotifier != nil {77 if a.reqNotifier != nil {
78 a.reqNotifier.login(entity.Tag())78 a.reqNotifier.login(entity.Tag())
@@ -81,11 +81,18 @@
81 // to serve to them.81 // to serve to them.
82 newRoot, err := a.apiRootForEntity(entity, c)82 newRoot, err := a.apiRootForEntity(entity, c)
83 if err != nil {83 if err != nil {
84 return err84 return params.LoginResult{}, err
85 }85 }
86
87 // Fetch the API server addresses from state.
88 hostPorts, err := a.root.srv.state.APIHostPorts()
89 if err != nil {
90 return params.LoginResult{}, err
91 }
92 logger.Debugf("hostPorts: %v", hostPorts)
8693
87 a.root.rpcConn.Serve(newRoot, serverError)94 a.root.rpcConn.Serve(newRoot, serverError)
88 return nil95 return params.LoginResult{hostPorts}, nil
89}96}
9097
91func checkCreds(st *state.State, c params.Creds) (taggedAuthenticator, error) {98func checkCreds(st *state.State, c params.Creds) (taggedAuthenticator, error) {
9299
=== modified file 'state/apiserver/login_test.go'
--- state/apiserver/login_test.go 2014-03-13 13:42:50 +0000
+++ state/apiserver/login_test.go 2014-03-31 12:32:07 +0000
@@ -4,10 +4,14 @@
4package apiserver_test4package apiserver_test
55
6import (6import (
7 "net"
8 "strconv"
9
7 "github.com/juju/loggo"10 "github.com/juju/loggo"
8 jc "github.com/juju/testing/checkers"11 jc "github.com/juju/testing/checkers"
9 gc "launchpad.net/gocheck"12 gc "launchpad.net/gocheck"
1013
14 "launchpad.net/juju-core/instance"
11 jujutesting "launchpad.net/juju-core/juju/testing"15 jujutesting "launchpad.net/juju-core/juju/testing"
12 "launchpad.net/juju-core/state"16 "launchpad.net/juju-core/state"
13 "launchpad.net/juju-core/state/api"17 "launchpad.net/juju-core/state/api"
@@ -160,8 +164,74 @@
160 // Now that we are logged in, we see the entity's tag164 // Now that we are logged in, we see the entity's tag
161 // [0-9.umns] is to handle timestamps that are ns, us, ms, or s165 // [0-9.umns] is to handle timestamps that are ns, us, ms, or s
162 // long, though we expect it to be in the 'ms' range.166 // long, though we expect it to be in the 'ms' range.
163 `-> \[[0-9A-F]+\] machine-0 [0-9.]+[umn]?s {"RequestId":1,"Response":{}} Admin\[""\].Login`,167 `-> \[[0-9A-F]+\] machine-0 [0-9.]+[umn]?s {"RequestId":1,"Response":{"Servers":\[\]}} Admin\[""\].Login`,
164 `<- \[[0-9A-F]+\] machine-0 {"RequestId":2,"Type":"Machiner","Request":"Life","Params":{"Entities":\[{"Tag":"machine-0"}\]}}`,168 `<- \[[0-9A-F]+\] machine-0 {"RequestId":2,"Type":"Machiner","Request":"Life","Params":{"Entities":\[{"Tag":"machine-0"}\]}}`,
165 `-> \[[0-9A-F]+\] machine-0 [0-9.umns]+ {"RequestId":2,"Response":{"Results":\[{"Life":"alive","Error":null}\]}} Machiner\[""\]\.Life`,169 `-> \[[0-9A-F]+\] machine-0 [0-9.umns]+ {"RequestId":2,"Response":{"Results":\[{"Life":"alive","Error":null}\]}} Machiner\[""\]\.Life`,
166 })170 })
167}171}
172
173func (s *loginSuite) TestLoginAddrs(c *gc.C) {
174 machine, err := s.State.AddMachine("quantal", state.JobHostUnits)
175 c.Assert(err, gc.IsNil)
176 err = machine.SetProvisioned("foo", "fake_nonce", nil)
177 c.Assert(err, gc.IsNil)
178 password, err := utils.RandomPassword()
179 c.Assert(err, gc.IsNil)
180 err = machine.SetPassword(password)
181 c.Assert(err, gc.IsNil)
182 info, cleanup := s.setupServer(c)
183 defer cleanup()
184 info.Tag = machine.Tag()
185 info.Password = password
186 info.Nonce = "fake_nonce"
187
188 // Initially just the address we connect with is returned,
189 // despite there being no APIHostPorts in state.
190 connectedAddr, hostPorts := s.loginHostPorts(c, info)
191 connectedAddrHost, connectedAddrPortString, err := net.SplitHostPort(connectedAddr)
192 c.Assert(err, gc.IsNil)
193 connectedAddrPort, err := strconv.Atoi(connectedAddrPortString)
194 c.Assert(err, gc.IsNil)
195 connectedAddrHostPorts := [][]instance.HostPort{
196 []instance.HostPort{{
197 instance.NewAddress(connectedAddrHost),
198 connectedAddrPort,
199 }},
200 }
201 c.Assert(hostPorts, gc.DeepEquals, connectedAddrHostPorts)
202
203 // After storing APIHostPorts in state, Login should store
204 // all of them and the address we connected with.
205 server1Addresses := []instance.Address{{
206 Value: "server-1",
207 Type: instance.HostName,
208 NetworkScope: instance.NetworkPublic,
209 }, {
210 Value: "10.0.0.1",
211 Type: instance.Ipv4Address,
212 NetworkName: "internal",
213 NetworkScope: instance.NetworkCloudLocal,
214 }}
215 server2Addresses := []instance.Address{{
216 Value: "::1",
217 Type: instance.Ipv6Address,
218 NetworkName: "loopback",
219 NetworkScope: instance.NetworkMachineLocal,
220 }}
221 stateAPIHostPorts := [][]instance.HostPort{
222 instance.AddressesWithPort(server1Addresses, 123),
223 instance.AddressesWithPort(server2Addresses, 456),
224 }
225 err = s.State.SetAPIHostPorts(stateAPIHostPorts)
226 c.Assert(err, gc.IsNil)
227 connectedAddr, hostPorts = s.loginHostPorts(c, info)
228 stateAPIHostPorts = append(stateAPIHostPorts, connectedAddrHostPorts...)
229 c.Assert(hostPorts, gc.DeepEquals, stateAPIHostPorts)
230}
231
232func (s *loginSuite) loginHostPorts(c *gc.C, info *api.Info) (connectedAddr string, hostPorts [][]instance.HostPort) {
233 st, err := api.Open(info, fastDialOpts)
234 c.Assert(err, gc.IsNil)
235 defer st.Close()
236 return st.Addr(), st.APIHostPorts()
237}

Subscribers

People subscribed via source and target branches

to status/vote changes: