Merge lp:~axwalk/juju-core/client-cache-apihostports into lp:~go-bot/juju-core/trunk
- client-cache-apihostports
- Merge into trunk
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 |
Related bugs: |
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.
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.
Andrew Wilkins (axwalk) wrote : | # |
Roger Peppe (rogpeppe) wrote : | # |
Great direction, thanks, with a few suggestions below.
https:/
File juju/api.go (right):
https:/
juju/api.go:252: logger.
This isn't right (the error might contain % characters).
logger.
(and change the error returned from cacheChangedAPI
so that the message won't be repetitious)
https:/
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:/
juju/api.go:356: sort.Strings(a)
I don't think I'd sort the addresses - the order can be significant.
https:/
File state/api/
https:/
state/api/
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:/
state/api/
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:/
File state/api/
https:/
state/api/
I think I'd just include the field explicitly here
rather than embedding.
https:/
File state/api/state.go (right):
https:/
state/api/
else
This comment seems like it might be more helpful to put as part of the
Addrs (or HostPorts) doc comment.
https:/
File state/apiserver
https:/
state/apiserver
I think it's perfectly reasonable to make the error fatal here.
If we can't access the APIHostPorts document, something is seriously
wrong.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
https:/
File juju/api.go (right):
https:/
juju/api.go:252: logger.
On 2014/03/28 10:02:12, rog wrote:
> This isn't right (the error might contain % characters).
> logger.
> (and change the error returned from cacheChangedAPI
> so that the message won't be repetitious)
I copied that from newAPIFromStore and clearly I didn't look too close.
Fixed both.
https:/
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:/
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:/
File state/api/
https:/
state/api/
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:/
state/api/
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:/
File state/api/
https:/
state/api/
On 2014/03/28 10:02:12, rog wrote:
> I think I'd just include the field explicitly here
> rather than embedding.
Done.
https:/
File state/api/state.go (right):
https:/
state/api/
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:/
File st...
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:/
File juju/apiconn_
https:/
juju/apiconn_
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:/
File state/api/
https:/
state/api/
[][]instance.
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:/
state/api/
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:/
File state/api/state.go (right):
https:/
state/api/
rather than have addAddrHostPort be a method that mutates st.hostPorts,
perhaps it might be clearer to have something like:
hostPorts, err := addAddress(
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.
([][]instance.
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.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
https:/
File juju/apiconn_
https:/
juju/apiconn_
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:/
File state/api/
https:/
state/api/
[][]instance.
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:/
state/api/
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:/
File state/api/state.go (right):
https:/
state/api/
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(
> 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.
> ([][]instance.
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.
Go Bot (go-bot) wrote : | # |
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.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
? launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
Roger Peppe (rogpeppe) wrote : | # |
still LGTM
https:/
File state/api/
https:/
state/api/
server...)
or hostPorts[i] = append(
(slightly shorter)
Preview Diff
1 | === modified file 'juju/api.go' |
2 | --- juju/api.go 2014-03-19 03:18:41 +0000 |
3 | +++ juju/api.go 2014-03-31 12:32:07 +0000 |
4 | @@ -14,6 +14,7 @@ |
5 | "launchpad.net/juju-core/environs/config" |
6 | "launchpad.net/juju-core/environs/configstore" |
7 | "launchpad.net/juju-core/errors" |
8 | + "launchpad.net/juju-core/instance" |
9 | "launchpad.net/juju-core/juju/osenv" |
10 | "launchpad.net/juju-core/names" |
11 | "launchpad.net/juju-core/state/api" |
12 | @@ -26,24 +27,25 @@ |
13 | // The following are variables so that they can be |
14 | // changed by tests. |
15 | var ( |
16 | - apiOpen = api.Open |
17 | - apiClose = (*api.State).Close |
18 | providerConnectDelay = 2 * time.Second |
19 | ) |
20 | |
21 | -// apiState wraps an api.State, redefining its Close method |
22 | -// so we can abuse it for testing purposes. |
23 | -type apiState struct { |
24 | - st *api.State |
25 | +// apiState provides a subset of api.State's public |
26 | +// interface, defined here so it can be mocked. |
27 | +type apiState interface { |
28 | + Close() error |
29 | + APIHostPorts() [][]instance.HostPort |
30 | +} |
31 | + |
32 | +type apiOpenFunc func(*api.Info, api.DialOpts) (apiState, error) |
33 | + |
34 | +type apiStateCachedInfo struct { |
35 | + apiState |
36 | // If cachedInfo is non-nil, it indicates that the info has been |
37 | // newly retrieved, and should be cached in the config store. |
38 | cachedInfo *api.Info |
39 | } |
40 | |
41 | -func (st apiState) Close() error { |
42 | - return apiClose(st.st) |
43 | -} |
44 | - |
45 | // APIConn holds a connection to a juju environment and its |
46 | // associated state through its API interface. |
47 | type APIConn struct { |
48 | @@ -62,7 +64,7 @@ |
49 | return nil, err |
50 | } |
51 | |
52 | - st, err := apiOpen(info, dialOpts) |
53 | + st, err := api.Open(info, dialOpts) |
54 | // TODO(rog): handle errUnauthorized when the API handles passwords. |
55 | if err != nil { |
56 | return nil, err |
57 | @@ -76,7 +78,7 @@ |
58 | // Close terminates the connection to the environment and releases |
59 | // any associated resources. |
60 | func (c *APIConn) Close() error { |
61 | - return apiClose(c.State) |
62 | + return c.State.Close() |
63 | } |
64 | |
65 | // NewAPIClientFromName returns an api.Client connected to the API Server for |
66 | @@ -112,12 +114,19 @@ |
67 | if err != nil { |
68 | return nil, err |
69 | } |
70 | - return newAPIFromStore(envName, store) |
71 | + apiOpen := func(info *api.Info, opts api.DialOpts) (apiState, error) { |
72 | + return api.Open(info, opts) |
73 | + } |
74 | + st, err := newAPIFromStore(envName, store, apiOpen) |
75 | + if err != nil { |
76 | + return nil, err |
77 | + } |
78 | + return st.(*api.State), nil |
79 | } |
80 | |
81 | // newAPIFromStore implements the bulk of NewAPIClientFromName |
82 | // but is separate for testing purposes. |
83 | -func newAPIFromStore(envName string, store configstore.Storage) (*api.State, error) { |
84 | +func newAPIFromStore(envName string, store configstore.Storage, apiOpen apiOpenFunc) (apiState, error) { |
85 | // Try to read the default environment configuration file. |
86 | // If it doesn't exist, we carry on in case |
87 | // there's some environment info for that environment. |
88 | @@ -170,7 +179,7 @@ |
89 | if info != nil && len(info.APIEndpoint().Addresses) > 0 { |
90 | logger.Debugf("trying cached API connection settings") |
91 | try.Start(func(stop <-chan struct{}) (io.Closer, error) { |
92 | - return apiInfoConnect(store, info, stop) |
93 | + return apiInfoConnect(store, info, apiOpen, stop) |
94 | }) |
95 | // Delay the config connection until we've spent |
96 | // some time trying to connect to the cached info. |
97 | @@ -179,7 +188,7 @@ |
98 | logger.Debugf("no cached API connection settings found") |
99 | } |
100 | try.Start(func(stop <-chan struct{}) (io.Closer, error) { |
101 | - return apiConfigConnect(info, envs, envName, stop, delay) |
102 | + return apiConfigConnect(info, envs, envName, apiOpen, stop, delay) |
103 | }) |
104 | try.Close() |
105 | val0, err := try.Result() |
106 | @@ -190,20 +199,23 @@ |
107 | } |
108 | return nil, err |
109 | } |
110 | + |
111 | val := val0.(apiState) |
112 | - |
113 | - if val.cachedInfo != nil && info != nil { |
114 | - // Cache the connection settings only if we used the |
115 | - // environment config, but any errors are just logged |
116 | - // as warnings, because they're not fatal. |
117 | - err = cacheAPIInfo(info, val.cachedInfo) |
118 | - if err != nil { |
119 | - logger.Warningf(err.Error()) |
120 | - } else { |
121 | - logger.Debugf("updated API connection settings cache") |
122 | + if cachedInfo, ok := val.(apiStateCachedInfo); ok { |
123 | + val = cachedInfo.apiState |
124 | + if cachedInfo.cachedInfo != nil && info != nil { |
125 | + // Cache the connection settings only if we used the |
126 | + // environment config, but any errors are just logged |
127 | + // as warnings, because they're not fatal. |
128 | + err = cacheAPIInfo(info, cachedInfo.cachedInfo) |
129 | + if err != nil { |
130 | + logger.Warningf("cannot cache API connection settings: %v", err.Error()) |
131 | + } else { |
132 | + logger.Infof("updated API connection settings cache") |
133 | + } |
134 | } |
135 | } |
136 | - return val.st, nil |
137 | + return val, nil |
138 | } |
139 | |
140 | func errorImportance(err error) int { |
141 | @@ -230,10 +242,10 @@ |
142 | |
143 | // apiInfoConnect looks for endpoint on the given environment and |
144 | // tries to connect to it, sending the result on the returned channel. |
145 | -func apiInfoConnect(store configstore.Storage, info configstore.EnvironInfo, stop <-chan struct{}) (apiState, error) { |
146 | +func apiInfoConnect(store configstore.Storage, info configstore.EnvironInfo, apiOpen apiOpenFunc, stop <-chan struct{}) (apiState, error) { |
147 | endpoint := info.APIEndpoint() |
148 | if info == nil || len(endpoint.Addresses) == 0 { |
149 | - return apiState{}, &infoConnectError{fmt.Errorf("no cached addresses")} |
150 | + return nil, &infoConnectError{fmt.Errorf("no cached addresses")} |
151 | } |
152 | logger.Infof("connecting to API addresses: %v", endpoint.Addresses) |
153 | apiInfo := &api.Info{ |
154 | @@ -244,9 +256,19 @@ |
155 | } |
156 | st, err := apiOpen(apiInfo, api.DefaultDialOpts()) |
157 | if err != nil { |
158 | - return apiState{}, &infoConnectError{err} |
159 | - } |
160 | - return apiState{st, nil}, err |
161 | + return nil, &infoConnectError{err} |
162 | + } |
163 | + var addrs []string |
164 | + for _, serverHostPorts := range st.APIHostPorts() { |
165 | + for _, hostPort := range serverHostPorts { |
166 | + addrs = append(addrs, hostPort.NetAddr()) |
167 | + } |
168 | + } |
169 | + // Update API addresses if they've changed. Error is non-fatal. |
170 | + if err := cacheChangedAPIAddresses(info, addrs); err != nil { |
171 | + logger.Warningf("cannot cache API addresses: %v", err.Error()) |
172 | + } |
173 | + return st, err |
174 | } |
175 | |
176 | // apiConfigConnect looks for configuration info on the given environment, |
177 | @@ -254,7 +276,7 @@ |
178 | // its endpoint. It only starts the attempt after the given delay, |
179 | // to allow the faster apiInfoConnect to hopefully succeed first. |
180 | // It returns nil if there was no configuration information found. |
181 | -func apiConfigConnect(info configstore.EnvironInfo, envs *environs.Environs, envName string, stop <-chan struct{}, delay time.Duration) (apiState, error) { |
182 | +func apiConfigConnect(info configstore.EnvironInfo, envs *environs.Environs, envName string, apiOpen apiOpenFunc, stop <-chan struct{}, delay time.Duration) (apiState, error) { |
183 | var cfg *config.Config |
184 | var err error |
185 | if info != nil && len(info.BootstrapConfig()) > 0 { |
186 | @@ -262,30 +284,30 @@ |
187 | } else if envs != nil { |
188 | cfg, err = envs.Config(envName) |
189 | if errors.IsNotFoundError(err) { |
190 | - return apiState{}, err |
191 | + return nil, err |
192 | } |
193 | } else { |
194 | - return apiState{}, errors.NotFoundf("environment %q", envName) |
195 | + return nil, errors.NotFoundf("environment %q", envName) |
196 | } |
197 | select { |
198 | case <-time.After(delay): |
199 | case <-stop: |
200 | - return apiState{}, errAborted |
201 | + return nil, errAborted |
202 | } |
203 | environ, err := environs.New(cfg) |
204 | if err != nil { |
205 | - return apiState{}, err |
206 | + return nil, err |
207 | } |
208 | apiInfo, err := environAPIInfo(environ) |
209 | if err != nil { |
210 | - return apiState{}, err |
211 | + return nil, err |
212 | } |
213 | st, err := apiOpen(apiInfo, api.DefaultDialOpts()) |
214 | // TODO(rog): handle errUnauthorized when the API handles passwords. |
215 | if err != nil { |
216 | - return apiState{}, err |
217 | + return nil, err |
218 | } |
219 | - return apiState{st, apiInfo}, nil |
220 | + return apiStateCachedInfo{st, apiInfo}, nil |
221 | } |
222 | |
223 | func environAPIInfo(environ environs.Environ) (*api.Info, error) { |
224 | @@ -312,14 +334,42 @@ |
225 | }) |
226 | _, username, err := names.ParseTag(apiInfo.Tag, names.UserTagKind) |
227 | if err != nil { |
228 | - return fmt.Errorf("not caching API connection settings: invalid API user tag: %v", err) |
229 | + return fmt.Errorf("invalid API user tag: %v", err) |
230 | } |
231 | info.SetAPICredentials(configstore.APICredentials{ |
232 | User: username, |
233 | Password: apiInfo.Password, |
234 | }) |
235 | + return info.Write() |
236 | +} |
237 | + |
238 | +// cacheChangedAPIAddresses updates the local environment settings (.jenv file) |
239 | +// with the provided API server addresses if they have changed. |
240 | +func cacheChangedAPIAddresses(info configstore.EnvironInfo, addrs []string) error { |
241 | + endpoint := info.APIEndpoint() |
242 | + if !addrsChanged(endpoint.Addresses, addrs) { |
243 | + return nil |
244 | + } |
245 | + logger.Debugf("API addresses changed from %q to %q", endpoint.Addresses, addrs) |
246 | + endpoint.Addresses = addrs |
247 | + info.SetAPIEndpoint(endpoint) |
248 | if err := info.Write(); err != nil { |
249 | - return fmt.Errorf("cannot cache API connection settings: %v", err) |
250 | + return err |
251 | } |
252 | + logger.Infof("updated API connection settings cache") |
253 | return nil |
254 | } |
255 | + |
256 | +// addrsChanged returns true iff the two |
257 | +// slices are not equal. Order is important. |
258 | +func addrsChanged(a, b []string) bool { |
259 | + if len(a) != len(b) { |
260 | + return true |
261 | + } |
262 | + for i := range a { |
263 | + if a[i] != b[i] { |
264 | + return true |
265 | + } |
266 | + } |
267 | + return false |
268 | +} |
269 | |
270 | === modified file 'juju/apiconn_test.go' |
271 | --- juju/apiconn_test.go 2014-03-19 22:16:15 +0000 |
272 | +++ juju/apiconn_test.go 2014-03-31 12:32:07 +0000 |
273 | @@ -8,7 +8,6 @@ |
274 | "os" |
275 | "time" |
276 | |
277 | - "github.com/juju/testing" |
278 | jc "github.com/juju/testing/checkers" |
279 | gc "launchpad.net/gocheck" |
280 | |
281 | @@ -18,6 +17,7 @@ |
282 | "launchpad.net/juju-core/environs/config" |
283 | "launchpad.net/juju-core/environs/configstore" |
284 | envtesting "launchpad.net/juju-core/environs/testing" |
285 | + "launchpad.net/juju-core/instance" |
286 | "launchpad.net/juju-core/juju" |
287 | "launchpad.net/juju-core/juju/osenv" |
288 | "launchpad.net/juju-core/provider/dummy" |
289 | @@ -131,8 +131,12 @@ |
290 | store := newConfigStore("noconfig", storeConfig) |
291 | |
292 | called := 0 |
293 | - expectState := new(api.State) |
294 | - apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (*api.State, error) { |
295 | + expectState := &mockAPIState{ |
296 | + apiHostPorts: [][]instance.HostPort{ |
297 | + instance.AddressesWithPort([]instance.Address{instance.NewAddress("0.1.2.3")}, 1234), |
298 | + }, |
299 | + } |
300 | + apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) { |
301 | c.Check(apiInfo.Tag, gc.Equals, "user-foo") |
302 | c.Check(string(apiInfo.CACert), gc.Equals, "certificated") |
303 | c.Check(apiInfo.Password, gc.Equals, "foopass") |
304 | @@ -140,14 +144,25 @@ |
305 | called++ |
306 | return expectState, nil |
307 | } |
308 | + |
309 | // Give NewAPIFromStore a store interface that can report when the |
310 | - // config was written to, to ensure the cache isn't updated. |
311 | - s.PatchValue(juju.APIOpen, apiOpen) |
312 | + // config was written to, to check if the cache is updated. |
313 | mockStore := &storageWithWriteNotify{store: store} |
314 | - st, err := juju.NewAPIFromStore("noconfig", mockStore) |
315 | + st, err := juju.NewAPIFromStore("noconfig", mockStore, apiOpen) |
316 | c.Assert(err, gc.IsNil) |
317 | c.Assert(st, gc.Equals, expectState) |
318 | c.Assert(called, gc.Equals, 1) |
319 | + c.Assert(mockStore.written, jc.IsTrue) |
320 | + info, err := store.ReadInfo("noconfig") |
321 | + c.Assert(err, gc.IsNil) |
322 | + c.Assert(info.APIEndpoint().Addresses, gc.DeepEquals, []string{"0.1.2.3:1234"}) |
323 | + mockStore.written = false |
324 | + |
325 | + // If APIHostPorts haven't changed, then the store won't be updated. |
326 | + st, err = juju.NewAPIFromStore("noconfig", mockStore, apiOpen) |
327 | + c.Assert(err, gc.IsNil) |
328 | + c.Assert(st, gc.Equals, expectState) |
329 | + c.Assert(called, gc.Equals, 2) |
330 | c.Assert(mockStore.written, jc.IsFalse) |
331 | } |
332 | |
333 | @@ -177,8 +192,8 @@ |
334 | c.Assert(info.APICredentials(), jc.DeepEquals, configstore.APICredentials{}) |
335 | |
336 | called := 0 |
337 | - expectState := new(api.State) |
338 | - apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (*api.State, error) { |
339 | + expectState := &mockAPIState{} |
340 | + apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) { |
341 | c.Check(apiInfo.Tag, gc.Equals, "user-admin") |
342 | c.Check(string(apiInfo.CACert), gc.Not(gc.Equals), "") |
343 | c.Check(apiInfo.Password, gc.Equals, "adminpass") |
344 | @@ -186,8 +201,7 @@ |
345 | called++ |
346 | return expectState, nil |
347 | } |
348 | - s.PatchValue(juju.APIOpen, apiOpen) |
349 | - st, err := juju.NewAPIFromStore("myenv", store) |
350 | + st, err := juju.NewAPIFromStore("myenv", store, apiOpen) |
351 | c.Assert(err, gc.IsNil) |
352 | c.Assert(st, gc.Equals, expectState) |
353 | c.Assert(called, gc.Equals, 1) |
354 | @@ -209,16 +223,11 @@ |
355 | defer coretesting.MakeEmptyFakeHome(c).Restore() |
356 | expectErr := fmt.Errorf("an error") |
357 | store := newConfigStoreWithError(expectErr) |
358 | - s.PatchValue(juju.APIOpen, panicAPIOpen) |
359 | - client, err := juju.NewAPIFromStore("noconfig", store) |
360 | + client, err := juju.NewAPIFromStore("noconfig", store, panicAPIOpen) |
361 | c.Assert(err, gc.Equals, expectErr) |
362 | c.Assert(client, gc.IsNil) |
363 | } |
364 | |
365 | -func panicAPIOpen(apiInfo *api.Info, opts api.DialOpts) (*api.State, error) { |
366 | - panic("api.Open called unexpectedly") |
367 | -} |
368 | - |
369 | func (s *NewAPIClientSuite) TestWithInfoNoAddresses(c *gc.C) { |
370 | defer coretesting.MakeEmptyFakeHome(c).Restore() |
371 | store := newConfigStore("noconfig", &environInfo{ |
372 | @@ -227,9 +236,7 @@ |
373 | CACert: "certificated", |
374 | }, |
375 | }) |
376 | - s.PatchValue(juju.APIOpen, panicAPIOpen) |
377 | - |
378 | - st, err := juju.NewAPIFromStore("noconfig", store) |
379 | + st, err := juju.NewAPIFromStore("noconfig", store, panicAPIOpen) |
380 | c.Assert(err, gc.ErrorMatches, `environment "noconfig" not found`) |
381 | c.Assert(st, gc.IsNil) |
382 | } |
383 | @@ -243,11 +250,10 @@ |
384 | }) |
385 | |
386 | expectErr := fmt.Errorf("an error") |
387 | - apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (*api.State, error) { |
388 | + apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) { |
389 | return nil, expectErr |
390 | } |
391 | - s.PatchValue(juju.APIOpen, apiOpen) |
392 | - st, err := juju.NewAPIFromStore("noconfig", store) |
393 | + st, err := juju.NewAPIFromStore("noconfig", store, apiOpen) |
394 | c.Assert(err, gc.Equals, expectErr) |
395 | c.Assert(st, gc.IsNil) |
396 | } |
397 | @@ -258,27 +264,30 @@ |
398 | bootstrapEnv(c, coretesting.SampleEnvName, store) |
399 | setEndpointAddress(c, store, coretesting.SampleEnvName, "infoapi.invalid") |
400 | |
401 | - infoOpenedState := new(api.State) |
402 | + infoOpenedState := &mockAPIState{} |
403 | infoEndpointOpened := make(chan struct{}) |
404 | - cfgOpenedState := new(api.State) |
405 | + cfgOpenedState := &mockAPIState{} |
406 | // On a sample run with no delay, the logic took 45ms to run, so |
407 | // we make the delay slightly more than that, so that if the |
408 | // logic doesn't delay at all, the test will fail reasonably consistently. |
409 | s.PatchValue(juju.ProviderConnectDelay, 50*time.Millisecond) |
410 | - apiOpen := func(info *api.Info, opts api.DialOpts) (*api.State, error) { |
411 | + apiOpen := func(info *api.Info, opts api.DialOpts) (juju.APIState, error) { |
412 | if info.Addrs[0] == "infoapi.invalid" { |
413 | infoEndpointOpened <- struct{}{} |
414 | return infoOpenedState, nil |
415 | } |
416 | return cfgOpenedState, nil |
417 | } |
418 | - s.PatchValue(juju.APIOpen, apiOpen) |
419 | |
420 | - stateClosed, restoreAPIClose := setAPIClosed() |
421 | - defer restoreAPIClose.Restore() |
422 | + stateClosed := make(chan juju.APIState) |
423 | + infoOpenedState.close = func(st juju.APIState) error { |
424 | + stateClosed <- st |
425 | + return nil |
426 | + } |
427 | + cfgOpenedState.close = infoOpenedState.close |
428 | |
429 | startTime := time.Now() |
430 | - st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store) |
431 | + st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store, apiOpen) |
432 | c.Assert(err, gc.IsNil) |
433 | // The connection logic should wait for some time before opening |
434 | // the API from the configuration. |
435 | @@ -320,13 +329,13 @@ |
436 | bootstrapEnv(c, coretesting.SampleEnvName, store) |
437 | setEndpointAddress(c, store, coretesting.SampleEnvName, "infoapi.invalid") |
438 | |
439 | - infoOpenedState := new(api.State) |
440 | + infoOpenedState := &mockAPIState{} |
441 | infoEndpointOpened := make(chan struct{}) |
442 | - cfgOpenedState := new(api.State) |
443 | + cfgOpenedState := &mockAPIState{} |
444 | cfgEndpointOpened := make(chan struct{}) |
445 | |
446 | s.PatchValue(juju.ProviderConnectDelay, 0*time.Second) |
447 | - apiOpen := func(info *api.Info, opts api.DialOpts) (*api.State, error) { |
448 | + apiOpen := func(info *api.Info, opts api.DialOpts) (juju.APIState, error) { |
449 | if info.Addrs[0] == "infoapi.invalid" { |
450 | infoEndpointOpened <- struct{}{} |
451 | <-infoEndpointOpened |
452 | @@ -336,14 +345,17 @@ |
453 | <-cfgEndpointOpened |
454 | return cfgOpenedState, nil |
455 | } |
456 | - s.PatchValue(juju.APIOpen, apiOpen) |
457 | |
458 | - stateClosed, restoreAPIClose := setAPIClosed() |
459 | - defer restoreAPIClose.Restore() |
460 | + stateClosed := make(chan juju.APIState) |
461 | + infoOpenedState.close = func(st juju.APIState) error { |
462 | + stateClosed <- st |
463 | + return nil |
464 | + } |
465 | + cfgOpenedState.close = infoOpenedState.close |
466 | |
467 | done := make(chan struct{}) |
468 | go func() { |
469 | - st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store) |
470 | + st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store, apiOpen) |
471 | c.Check(err, gc.IsNil) |
472 | c.Check(st, gc.Equals, infoOpenedState) |
473 | close(done) |
474 | @@ -387,14 +399,13 @@ |
475 | setEndpointAddress(c, store, coretesting.SampleEnvName, "infoapi.invalid") |
476 | |
477 | s.PatchValue(juju.ProviderConnectDelay, 0*time.Second) |
478 | - apiOpen := func(info *api.Info, opts api.DialOpts) (*api.State, error) { |
479 | + apiOpen := func(info *api.Info, opts api.DialOpts) (juju.APIState, error) { |
480 | if info.Addrs[0] == "infoapi.invalid" { |
481 | return nil, fmt.Errorf("info connect failed") |
482 | } |
483 | return nil, fmt.Errorf("config connect failed") |
484 | } |
485 | - s.PatchValue(juju.APIOpen, apiOpen) |
486 | - st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store) |
487 | + st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store, apiOpen) |
488 | c.Check(err, gc.ErrorMatches, "config connect failed") |
489 | c.Check(st, gc.IsNil) |
490 | } |
491 | @@ -427,7 +438,10 @@ |
492 | err = os.Remove(osenv.JujuHomePath("environments.yaml")) |
493 | c.Assert(err, gc.IsNil) |
494 | |
495 | - st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store) |
496 | + apiOpen := func(*api.Info, api.DialOpts) (juju.APIState, error) { |
497 | + return &mockAPIState{}, nil |
498 | + } |
499 | + st, err := juju.NewAPIFromStore(coretesting.SampleEnvName, store, apiOpen) |
500 | c.Check(err, gc.IsNil) |
501 | st.Close() |
502 | } |
503 | @@ -455,7 +469,10 @@ |
504 | // Now we have info for envName2 which will actually |
505 | // cause a connection to the originally bootstrapped |
506 | // state. |
507 | - st, err := juju.NewAPIFromStore(envName2, store) |
508 | + apiOpen := func(*api.Info, api.DialOpts) (juju.APIState, error) { |
509 | + return &mockAPIState{}, nil |
510 | + } |
511 | + st, err := juju.NewAPIFromStore(envName2, store, apiOpen) |
512 | c.Check(err, gc.IsNil) |
513 | st.Close() |
514 | |
515 | @@ -478,19 +495,6 @@ |
516 | c.Assert(envInfo.Name, gc.Equals, expectName) |
517 | } |
518 | |
519 | -func setAPIClosed() (<-chan *api.State, testing.Restorer) { |
520 | - stateClosed := make(chan *api.State) |
521 | - apiClose := func(st *api.State) error { |
522 | - stateClosed <- st |
523 | - return nil |
524 | - } |
525 | - return stateClosed, testing.PatchValue(juju.APIClose, apiClose) |
526 | -} |
527 | - |
528 | -func updateSecretsNoop(_ environs.Environ, _ *api.State) error { |
529 | - return nil |
530 | -} |
531 | - |
532 | // newConfigStoreWithError that will return the given |
533 | // error from ReadInfo. |
534 | func newConfigStoreWithError(err error) configstore.Storage { |
535 | |
536 | === modified file 'juju/export_test.go' |
537 | --- juju/export_test.go 2014-03-19 03:18:41 +0000 |
538 | +++ juju/export_test.go 2014-03-31 12:32:07 +0000 |
539 | @@ -1,8 +1,21 @@ |
540 | package juju |
541 | |
542 | +import ( |
543 | + "launchpad.net/juju-core/environs/configstore" |
544 | + "launchpad.net/juju-core/state/api" |
545 | +) |
546 | + |
547 | var ( |
548 | - APIOpen = &apiOpen |
549 | - APIClose = &apiClose |
550 | ProviderConnectDelay = &providerConnectDelay |
551 | - NewAPIFromStore = newAPIFromStore |
552 | ) |
553 | + |
554 | +type APIState apiState |
555 | + |
556 | +type APIOpenFunc func(*api.Info, api.DialOpts) (APIState, error) |
557 | + |
558 | +func NewAPIFromStore(envName string, store configstore.Storage, f APIOpenFunc) (APIState, error) { |
559 | + apiOpen := func(info *api.Info, opts api.DialOpts) (apiState, error) { |
560 | + return f(info, opts) |
561 | + } |
562 | + return newAPIFromStore(envName, store, apiOpen) |
563 | +} |
564 | |
565 | === added file 'juju/mock_test.go' |
566 | --- juju/mock_test.go 1970-01-01 00:00:00 +0000 |
567 | +++ juju/mock_test.go 2014-03-31 12:32:07 +0000 |
568 | @@ -0,0 +1,28 @@ |
569 | +package juju_test |
570 | + |
571 | +import ( |
572 | + "launchpad.net/juju-core/instance" |
573 | + "launchpad.net/juju-core/juju" |
574 | + "launchpad.net/juju-core/state/api" |
575 | +) |
576 | + |
577 | +type mockAPIState struct { |
578 | + close func(juju.APIState) error |
579 | + |
580 | + apiHostPorts [][]instance.HostPort |
581 | +} |
582 | + |
583 | +func (s *mockAPIState) Close() error { |
584 | + if s.close != nil { |
585 | + return s.close(s) |
586 | + } |
587 | + return nil |
588 | +} |
589 | + |
590 | +func (s *mockAPIState) APIHostPorts() [][]instance.HostPort { |
591 | + return s.apiHostPorts |
592 | +} |
593 | + |
594 | +func panicAPIOpen(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) { |
595 | + panic("api.Open called unexpectedly") |
596 | +} |
597 | |
598 | === modified file 'state/api/apiclient.go' |
599 | --- state/api/apiclient.go 2014-03-19 03:18:41 +0000 |
600 | +++ state/api/apiclient.go 2014-03-31 12:32:07 +0000 |
601 | @@ -11,6 +11,7 @@ |
602 | "code.google.com/p/go.net/websocket" |
603 | |
604 | "launchpad.net/juju-core/cert" |
605 | + "launchpad.net/juju-core/instance" |
606 | "launchpad.net/juju-core/log" |
607 | "launchpad.net/juju-core/rpc" |
608 | "launchpad.net/juju-core/rpc/jsoncodec" |
609 | @@ -25,7 +26,13 @@ |
610 | type State struct { |
611 | client *rpc.Conn |
612 | conn *websocket.Conn |
613 | - addr string |
614 | + |
615 | + // addr is the address used to connect to the API server. |
616 | + addr string |
617 | + |
618 | + // hostPorts is the API server addresses returned from Login, |
619 | + // which the client may cache and use for failover. |
620 | + hostPorts [][]instance.HostPort |
621 | |
622 | // authTag holds the authenticated entity's tag after login. |
623 | authTag string |
624 | @@ -189,7 +196,23 @@ |
625 | return s.client |
626 | } |
627 | |
628 | -// Addr returns the address used to connect to the RPC server. |
629 | +// Addr returns the address used to connect to the API server. |
630 | func (s *State) Addr() string { |
631 | return s.addr |
632 | } |
633 | + |
634 | +// APIHostPorts returns addresses that may be used to connect |
635 | +// to the API server, including the address used to connect. |
636 | +// |
637 | +// The addresses are scoped (public, cloud-internal, etc.), so |
638 | +// the client may choose which addresses to attempt. For the |
639 | +// Juju CLI, all addresses must be attempted, as the CLI may |
640 | +// be invoked both within and outside the environment (think |
641 | +// private clouds). |
642 | +func (s *State) APIHostPorts() [][]instance.HostPort { |
643 | + hostPorts := make([][]instance.HostPort, len(s.hostPorts)) |
644 | + for i, server := range s.hostPorts { |
645 | + hostPorts[i] = append([]instance.HostPort{}, server...) |
646 | + } |
647 | + return hostPorts |
648 | +} |
649 | |
650 | === modified file 'state/api/params/params.go' |
651 | --- state/api/params/params.go 2014-03-28 13:31:26 +0000 |
652 | +++ state/api/params/params.go 2014-03-31 12:32:07 +0000 |
653 | @@ -640,6 +640,11 @@ |
654 | Servers [][]instance.HostPort |
655 | } |
656 | |
657 | +// LoginResult holds the result of a Login call. |
658 | +type LoginResult struct { |
659 | + Servers [][]instance.HostPort |
660 | +} |
661 | + |
662 | // EnsureAvailability contains arguments for |
663 | // the EnsureAvailability client API call. |
664 | type EnsureAvailability struct { |
665 | |
666 | === modified file 'state/api/state.go' |
667 | --- state/api/state.go 2014-02-27 05:24:43 +0000 |
668 | +++ state/api/state.go 2014-03-31 12:32:07 +0000 |
669 | @@ -4,6 +4,10 @@ |
670 | package api |
671 | |
672 | import ( |
673 | + "net" |
674 | + "strconv" |
675 | + |
676 | + "launchpad.net/juju-core/instance" |
677 | "launchpad.net/juju-core/state/api/agent" |
678 | "launchpad.net/juju-core/state/api/charmrevisionupdater" |
679 | "launchpad.net/juju-core/state/api/deployer" |
680 | @@ -24,17 +28,54 @@ |
681 | // method is usually called automatically by Open. The machine nonce |
682 | // should be empty unless logging in as a machine agent. |
683 | func (st *State) Login(tag, password, nonce string) error { |
684 | + var result params.LoginResult |
685 | err := st.Call("Admin", "", "Login", ¶ms.Creds{ |
686 | AuthTag: tag, |
687 | Password: password, |
688 | Nonce: nonce, |
689 | - }, nil) |
690 | + }, &result) |
691 | if err == nil { |
692 | st.authTag = tag |
693 | + hostPorts, err := addAddress(result.Servers, st.addr) |
694 | + if err != nil { |
695 | + st.Close() |
696 | + return err |
697 | + } |
698 | + st.hostPorts = hostPorts |
699 | } |
700 | return err |
701 | } |
702 | |
703 | +// addAddress appends a new server derived from the given |
704 | +// address to servers if the address is not already found |
705 | +// there. |
706 | +func addAddress(servers [][]instance.HostPort, addr string) ([][]instance.HostPort, error) { |
707 | + for _, server := range servers { |
708 | + for _, hostPort := range server { |
709 | + if hostPort.NetAddr() == addr { |
710 | + return servers, nil |
711 | + } |
712 | + } |
713 | + } |
714 | + host, portString, err := net.SplitHostPort(addr) |
715 | + if err != nil { |
716 | + return nil, err |
717 | + } |
718 | + port, err := strconv.Atoi(portString) |
719 | + if err != nil { |
720 | + return nil, err |
721 | + } |
722 | + hostPort := instance.HostPort{ |
723 | + Address: instance.Address{ |
724 | + Value: host, |
725 | + Type: instance.DeriveAddressType(host), |
726 | + NetworkScope: instance.NetworkUnknown, |
727 | + }, |
728 | + Port: port, |
729 | + } |
730 | + return append(servers, []instance.HostPort{hostPort}), nil |
731 | +} |
732 | + |
733 | // Client returns an object that can be used |
734 | // to access client-specific functionality. |
735 | func (st *State) Client() *Client { |
736 | |
737 | === modified file 'state/apiserver/admin.go' |
738 | --- state/apiserver/admin.go 2014-01-17 19:33:42 +0000 |
739 | +++ state/apiserver/admin.go 2014-03-31 12:32:07 +0000 |
740 | @@ -63,16 +63,16 @@ |
741 | // Login logs in with the provided credentials. |
742 | // All subsequent requests on the connection will |
743 | // act as the authenticated user. |
744 | -func (a *srvAdmin) Login(c params.Creds) error { |
745 | +func (a *srvAdmin) Login(c params.Creds) (params.LoginResult, error) { |
746 | a.mu.Lock() |
747 | defer a.mu.Unlock() |
748 | if a.loggedIn { |
749 | // This can only happen if Login is called concurrently. |
750 | - return errAlreadyLoggedIn |
751 | + return params.LoginResult{}, errAlreadyLoggedIn |
752 | } |
753 | entity, err := checkCreds(a.root.srv.state, c) |
754 | if err != nil { |
755 | - return err |
756 | + return params.LoginResult{}, err |
757 | } |
758 | if a.reqNotifier != nil { |
759 | a.reqNotifier.login(entity.Tag()) |
760 | @@ -81,11 +81,18 @@ |
761 | // to serve to them. |
762 | newRoot, err := a.apiRootForEntity(entity, c) |
763 | if err != nil { |
764 | - return err |
765 | - } |
766 | + return params.LoginResult{}, err |
767 | + } |
768 | + |
769 | + // Fetch the API server addresses from state. |
770 | + hostPorts, err := a.root.srv.state.APIHostPorts() |
771 | + if err != nil { |
772 | + return params.LoginResult{}, err |
773 | + } |
774 | + logger.Debugf("hostPorts: %v", hostPorts) |
775 | |
776 | a.root.rpcConn.Serve(newRoot, serverError) |
777 | - return nil |
778 | + return params.LoginResult{hostPorts}, nil |
779 | } |
780 | |
781 | func checkCreds(st *state.State, c params.Creds) (taggedAuthenticator, error) { |
782 | |
783 | === modified file 'state/apiserver/login_test.go' |
784 | --- state/apiserver/login_test.go 2014-03-13 13:42:50 +0000 |
785 | +++ state/apiserver/login_test.go 2014-03-31 12:32:07 +0000 |
786 | @@ -4,10 +4,14 @@ |
787 | package apiserver_test |
788 | |
789 | import ( |
790 | + "net" |
791 | + "strconv" |
792 | + |
793 | "github.com/juju/loggo" |
794 | jc "github.com/juju/testing/checkers" |
795 | gc "launchpad.net/gocheck" |
796 | |
797 | + "launchpad.net/juju-core/instance" |
798 | jujutesting "launchpad.net/juju-core/juju/testing" |
799 | "launchpad.net/juju-core/state" |
800 | "launchpad.net/juju-core/state/api" |
801 | @@ -160,8 +164,74 @@ |
802 | // Now that we are logged in, we see the entity's tag |
803 | // [0-9.umns] is to handle timestamps that are ns, us, ms, or s |
804 | // long, though we expect it to be in the 'ms' range. |
805 | - `-> \[[0-9A-F]+\] machine-0 [0-9.]+[umn]?s {"RequestId":1,"Response":{}} Admin\[""\].Login`, |
806 | + `-> \[[0-9A-F]+\] machine-0 [0-9.]+[umn]?s {"RequestId":1,"Response":{"Servers":\[\]}} Admin\[""\].Login`, |
807 | `<- \[[0-9A-F]+\] machine-0 {"RequestId":2,"Type":"Machiner","Request":"Life","Params":{"Entities":\[{"Tag":"machine-0"}\]}}`, |
808 | `-> \[[0-9A-F]+\] machine-0 [0-9.umns]+ {"RequestId":2,"Response":{"Results":\[{"Life":"alive","Error":null}\]}} Machiner\[""\]\.Life`, |
809 | }) |
810 | } |
811 | + |
812 | +func (s *loginSuite) TestLoginAddrs(c *gc.C) { |
813 | + machine, err := s.State.AddMachine("quantal", state.JobHostUnits) |
814 | + c.Assert(err, gc.IsNil) |
815 | + err = machine.SetProvisioned("foo", "fake_nonce", nil) |
816 | + c.Assert(err, gc.IsNil) |
817 | + password, err := utils.RandomPassword() |
818 | + c.Assert(err, gc.IsNil) |
819 | + err = machine.SetPassword(password) |
820 | + c.Assert(err, gc.IsNil) |
821 | + info, cleanup := s.setupServer(c) |
822 | + defer cleanup() |
823 | + info.Tag = machine.Tag() |
824 | + info.Password = password |
825 | + info.Nonce = "fake_nonce" |
826 | + |
827 | + // Initially just the address we connect with is returned, |
828 | + // despite there being no APIHostPorts in state. |
829 | + connectedAddr, hostPorts := s.loginHostPorts(c, info) |
830 | + connectedAddrHost, connectedAddrPortString, err := net.SplitHostPort(connectedAddr) |
831 | + c.Assert(err, gc.IsNil) |
832 | + connectedAddrPort, err := strconv.Atoi(connectedAddrPortString) |
833 | + c.Assert(err, gc.IsNil) |
834 | + connectedAddrHostPorts := [][]instance.HostPort{ |
835 | + []instance.HostPort{{ |
836 | + instance.NewAddress(connectedAddrHost), |
837 | + connectedAddrPort, |
838 | + }}, |
839 | + } |
840 | + c.Assert(hostPorts, gc.DeepEquals, connectedAddrHostPorts) |
841 | + |
842 | + // After storing APIHostPorts in state, Login should store |
843 | + // all of them and the address we connected with. |
844 | + server1Addresses := []instance.Address{{ |
845 | + Value: "server-1", |
846 | + Type: instance.HostName, |
847 | + NetworkScope: instance.NetworkPublic, |
848 | + }, { |
849 | + Value: "10.0.0.1", |
850 | + Type: instance.Ipv4Address, |
851 | + NetworkName: "internal", |
852 | + NetworkScope: instance.NetworkCloudLocal, |
853 | + }} |
854 | + server2Addresses := []instance.Address{{ |
855 | + Value: "::1", |
856 | + Type: instance.Ipv6Address, |
857 | + NetworkName: "loopback", |
858 | + NetworkScope: instance.NetworkMachineLocal, |
859 | + }} |
860 | + stateAPIHostPorts := [][]instance.HostPort{ |
861 | + instance.AddressesWithPort(server1Addresses, 123), |
862 | + instance.AddressesWithPort(server2Addresses, 456), |
863 | + } |
864 | + err = s.State.SetAPIHostPorts(stateAPIHostPorts) |
865 | + c.Assert(err, gc.IsNil) |
866 | + connectedAddr, hostPorts = s.loginHostPorts(c, info) |
867 | + stateAPIHostPorts = append(stateAPIHostPorts, connectedAddrHostPorts...) |
868 | + c.Assert(hostPorts, gc.DeepEquals, stateAPIHostPorts) |
869 | +} |
870 | + |
871 | +func (s *loginSuite) loginHostPorts(c *gc.C, info *api.Info) (connectedAddr string, hostPorts [][]instance.HostPort) { |
872 | + st, err := api.Open(info, fastDialOpts) |
873 | + c.Assert(err, gc.IsNil) |
874 | + defer st.Close() |
875 | + return st.Addr(), st.APIHostPorts() |
876 | +} |
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-apihostpo rts/+merge/ 213217
Requires: /code.launchpad .net/~axwalk/ juju-core/ apihostports- api/+merge/ 213200
https:/
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/81780043/
Affected files (+173, -11 lines): test.go apiclient. go params/ params. go /admin. go /login_ test.go
A [revision details]
M juju/api.go
M juju/apiconn_
M state/api/
M state/api/
M state/api/state.go
M state/apiserver
M state/apiserver