Merge lp:~jameinel/juju-core/login-env-urls into lp:~go-bot/juju-core/trunk
- login-env-urls
- Merge into trunk
Status: | Merged |
---|---|
Merge reported by: | John A Meinel |
Merged at revision: | not available |
Proposed branch: | lp:~jameinel/juju-core/login-env-urls |
Merge into: | lp:~go-bot/juju-core/trunk |
Diff against target: |
1784 lines (+941/-101) (has conflicts) 29 files modified
dependencies.tsv (+1/-0) environs/configstore/disk.go (+5/-2) environs/configstore/interface.go (+4/-0) environs/configstore/interface_test.go (+6/-4) juju/api.go (+42/-14) juju/apiconn_test.go (+139/-3) juju/mock_test.go (+5/-0) state/api/apiclient.go (+41/-6) state/api/apiclient_test.go (+102/-1) state/api/client_test.go (+69/-6) state/api/export_test.go (+1/-0) state/api/params/params.go (+2/-1) state/api/state.go (+1/-0) state/api/state_test.go (+21/-0) state/apiserver/admin.go (+21/-1) state/apiserver/apiserver.go (+84/-22) state/apiserver/charms.go (+4/-0) state/apiserver/charms_test.go (+93/-3) state/apiserver/common/errors.go (+19/-0) state/apiserver/common/errors_test.go (+9/-0) state/apiserver/debuglog.go (+5/-0) state/apiserver/debuglog_test.go (+54/-13) state/apiserver/export_test.go (+4/-0) state/apiserver/httphandler.go (+26/-0) state/apiserver/login_test.go (+30/-5) state/apiserver/root_test.go (+28/-0) state/apiserver/server_test.go (+51/-0) state/apiserver/tools.go (+4/-0) state/apiserver/tools_test.go (+70/-20) Text conflict in state/api/apiclient_test.go |
To merge this branch: | bzr merge lp:~jameinel/juju-core/login-env-urls |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+221632@code.launchpad.net |
Commit message
Description of the change
state/*: login to /ENVUUID/api URLs
This is an evolution of:
https:/
https:/
Instead of having Login itself take an Environment Tag, it changes our
API so that we connect to a different URL. Instead of
"wss://
"wss://
The change itself involves a few variables:
1) A new dependency "github.
simple library that lets you put in patterns to your http.Mux, which
will then transform "/:path/foo" as matching anything in ":path" and
adding that as a Query argument instead.
2) Using that library to expose /:environ/api and .../log, .../charms,
.../tools. This patch itself doesn't make use of the new URLs, because
it is not backwards compatible, and I didn't want to quite sort out how
to do the backwards compatibility yet.
3) We do use the environ/api one when we know the Environment UUID,
which Login will return for us (and we will cache).
I did test that this still works against a 1.18 server (it connects to
the /ENVUUID/api address, but as that is a child of / it still works). I
also ran into something a bit surprising, but probably ok. If you
actually force the environ-uuid to be invalid in your .jenv file, the
code will try to connect, and fail with InvalidEnviron. However, our
connection code has lots of resilience built in, so we end up just
falling back to the Config fallback open, which naturally logs in as
just environUUID="", which then finds the right environment UUID and
caches it.
So this code doesn't fix all possible paths, but I think it improves the
state of the world that I'd like to land it and get back to API
versioning.
John A Meinel (jameinel) wrote : | # |
- 2818. By John A Meinel
-
review changes from AXW's review.
- 2819. By John A Meinel
-
merge trunk, resolve conflicts
John A Meinel (jameinel) wrote : | # |
Please take a look.
Roger Peppe (rogpeppe) wrote : | # |
Looks great in general, with a few queries and suggestions.
https:/
File environs/
https:/
environs/
Why is this not an environ tag, to match the tag in api.Info ?
https:/
File state/api/
https:/
state/api/
why not just use environ tag throughout?
https:/
File state/api/
https:/
state/api/
s/Setup/SetUp/
?
https:/
File state/apiserver
https:/
state/apiserver
One passing thought - we might be slightly more future proof and
"obviously right" if the path was "/environ/
need to rely on the fact that environ uuids can never conflict with any
other path.
If you do that, then bmizerany/pat becomes pretty much redundant,
because it's almost as simple to just parse the first element of the URL
path as a uuid.
https:/
state/apiserver
responses.
That seems like a reason to avoid bmizerany/pat - if someone sends a
method we don't understand, it would be nice to get a standard error
back. handleAll isn't exhaustive.
https:/
state/apiserver
http.HandlerFun
Can we please deprecate the behaviour that addressing any undefined url
gives us the API? It was always unintentional, and now I believe it's
actively harmful. It should be pretty simple to do - just write a
handler that 404's any path that's not /, and register it to /.
https:/
state/apiserver
UUID is currently
s/firt/first/
https:/
state/apiserver
Rather than add an extra mongo round trip to every http request, we
could store the UUID in the server.
https:/
File state/apiserver
https:/
state/apiserver
stder...
Horacio Durán (hduran-8) wrote : | # |
On 2014/06/01 09:43:06, jameinel wrote:
> Please take a look.
LGTM if rog comments are addressed.
- 2820. By John A Meinel
-
rename setup to setUp and Setup to SetUp
- 2821. By John A Meinel
-
cache the environmentUUID to avoid extra DB requests on API requests.
- 2822. By John A Meinel
-
custom error type.
- 2823. By John A Meinel
-
add a test that we don't allow any-old-path
John A Meinel (jameinel) wrote : | # |
I had written this up, but it didn't get published because I switched to
git.
https:/
File environs/
https:/
environs/
On 2014/06/02 10:08:17, rog wrote:
> Why is this not an environ tag, to match the tag in api.Info ?
This is driven primarily from William's strong desire that "tags are in
the API, but don't bleed into the rest of the system".
Specifically, in the .jenv file we store "user" but and not "tag",
though api.Info does have a Tag field and not a User field.
So it generally is:
a) raw UUID on disk and in state
b) Tag when passed around the API structures.
I'm considering changing the URL to be /ENVTAG/api instead of
/ENVUUID/api. I'm letting that simmer a bit before I make a final
decision.
https:/
File state/api/
https:/
state/api/
On 2014/06/02 10:08:17, rog wrote:
> why not just use environ tag throughout?
as mentioned before, this follows the same pattern we use for User vs
Tag, etc.
https:/
File state/api/
https:/
state/api/
On 2014/06/02 10:08:17, rog wrote:
> s/Setup/SetUp/
> ?
Done.
https:/
File state/apiserver
https:/
state/apiserver
http.HandlerFun
On 2014/06/02 10:08:18, rog wrote:
> Can we please deprecate the behaviour that addressing any undefined
url gives us
> the API? It was always unintentional, and now I believe it's actively
harmful.
> It should be pretty simple to do - just write a handler that 404's any
path
> that's not /, and register it to /.
For compatibility, we still have to serve the API at /. I believe
bmizerany actually does 404 things that aren't at /, but we can write
some tests to be clear about it.
https:/
state/apiserver
On 2014/06/02 10:08:18, rog wrote:
> Rather than add an extra mongo round trip to every http request, we
could store
> the UUID in the server.
Done.
https:/
File state/apiserver
https:/
state/apiserver
stderrors.
On 2014/06/02 10:08:18, rog wrote:
> ErrUnknownEnv...
Roger Peppe (rogpeppe) wrote : | # |
https:/
File state/apiserver
https:/
state/apiserver
http.HandlerFun
On 2014/06/05 13:47:19, jameinel wrote:
> On 2014/06/02 10:08:18, rog wrote:
> > Can we please deprecate the behaviour that addressing any undefined
url gives
> us
> > the API? It was always unintentional, and now I believe it's
actively harmful.
> > It should be pretty simple to do - just write a handler that 404's
any path
> > that's not /, and register it to /.
> For compatibility, we still have to serve the API at /. I believe
bmizerany
> actually does 404 things that aren't at /, but we can write some tests
to be
> clear about it.
I haven't tried it, but from the examples in the godoc, it
looks like that's not actually the case.
For example (from the godoc):
/hello/:name/
Will match:
/hello/blake/
/hello/keith/foo
Preview Diff
1 | === modified file 'dependencies.tsv' |
2 | --- dependencies.tsv 2014-06-02 15:47:39 +0000 |
3 | +++ dependencies.tsv 2014-06-05 04:28:36 +0000 |
4 | @@ -3,6 +3,7 @@ |
5 | github.com/binary132/gojsonpointer git 57ab5e9c764219a3e0c4d7759797fefdcab22e9c |
6 | github.com/binary132/gojsonreference git 75785fb7b21f9bf2051dca600da83ff57bc6582a |
7 | github.com/binary132/gojsonschema git 640782bf48d45ba2b22fd1e8a80842667c52af00 |
8 | +github.com/bmizerany/pat git 48be7df2c27e1cec821a3284a683ce6ef90d9052 |
9 | github.com/joyent/gocommon git 40c7818502f7c1ebbb13dab185a26e77b746ff40 |
10 | github.com/joyent/gomanta git cabd97b029d931836571f00b7e48c331809a30b7 |
11 | github.com/joyent/gosdc git 2f11feadd2d9891e92296a1077c3e2e56939547d |
12 | |
13 | === modified file 'environs/configstore/disk.go' |
14 | --- environs/configstore/disk.go 2014-05-13 04:50:10 +0000 |
15 | +++ environs/configstore/disk.go 2014-06-05 04:28:36 +0000 |
16 | @@ -32,6 +32,7 @@ |
17 | type EnvironInfoData struct { |
18 | User string |
19 | Password string |
20 | + EnvironUUID string `json:"environ-uuid,omitempty" yaml:"environ-uuid,omitempty"` |
21 | StateServers []string `json:"state-servers" yaml:"state-servers"` |
22 | CACert string `json:"ca-cert" yaml:"ca-cert"` |
23 | Config map[string]interface{} `json:"bootstrap-config,omitempty" yaml:"bootstrap-config,omitempty"` |
24 | @@ -138,8 +139,9 @@ |
25 | // APIEndpoint implements EnvironInfo.APIEndpoint. |
26 | func (info *environInfo) APIEndpoint() APIEndpoint { |
27 | return APIEndpoint{ |
28 | - Addresses: info.EnvInfo.StateServers, |
29 | - CACert: info.EnvInfo.CACert, |
30 | + Addresses: info.EnvInfo.StateServers, |
31 | + CACert: info.EnvInfo.CACert, |
32 | + EnvironUUID: info.EnvInfo.EnvironUUID, |
33 | } |
34 | } |
35 | |
36 | @@ -155,6 +157,7 @@ |
37 | func (info *environInfo) SetAPIEndpoint(endpoint APIEndpoint) { |
38 | info.EnvInfo.StateServers = endpoint.Addresses |
39 | info.EnvInfo.CACert = endpoint.CACert |
40 | + info.EnvInfo.EnvironUUID = endpoint.EnvironUUID |
41 | } |
42 | |
43 | // SetAPICredentials implements EnvironInfo.SetAPICredentials. |
44 | |
45 | === modified file 'environs/configstore/interface.go' |
46 | --- environs/configstore/interface.go 2014-05-22 00:48:08 +0000 |
47 | +++ environs/configstore/interface.go 2014-06-05 04:28:36 +0000 |
48 | @@ -19,6 +19,10 @@ |
49 | // CACert holds the CA certificate that |
50 | // signed the API server's key. |
51 | CACert string |
52 | + |
53 | + // EnvironUUID holds the UUID for the environment we are connecting to. |
54 | + // This may be empty if the environment has not been bootstrapped. |
55 | + EnvironUUID string |
56 | } |
57 | |
58 | // APICredentials hold credentials for connecting to an API endpoint. |
59 | |
60 | === modified file 'environs/configstore/interface_test.go' |
61 | --- environs/configstore/interface_test.go 2014-05-20 04:27:02 +0000 |
62 | +++ environs/configstore/interface_test.go 2014-06-05 04:28:36 +0000 |
63 | @@ -46,8 +46,9 @@ |
64 | c.Assert(err, gc.IsNil) |
65 | |
66 | expectEndpoint := configstore.APIEndpoint{ |
67 | - Addresses: []string{"example.com"}, |
68 | - CACert: "a cert", |
69 | + Addresses: []string{"example.com"}, |
70 | + CACert: "a cert", |
71 | + EnvironUUID: "dead-beef", |
72 | } |
73 | info.SetAPIEndpoint(expectEndpoint) |
74 | c.Assert(info.APIEndpoint(), gc.DeepEquals, expectEndpoint) |
75 | @@ -75,8 +76,9 @@ |
76 | info.SetAPICredentials(expectCreds) |
77 | |
78 | expectEndpoint := configstore.APIEndpoint{ |
79 | - Addresses: []string{"example.com"}, |
80 | - CACert: "a cert", |
81 | + Addresses: []string{"example.invalid"}, |
82 | + CACert: "a cert", |
83 | + EnvironUUID: "dead-beef", |
84 | } |
85 | info.SetAPIEndpoint(expectEndpoint) |
86 | |
87 | |
88 | === modified file 'juju/api.go' |
89 | --- juju/api.go 2014-05-13 04:30:48 +0000 |
90 | +++ juju/api.go 2014-06-05 04:28:36 +0000 |
91 | @@ -32,6 +32,7 @@ |
92 | type apiState interface { |
93 | Close() error |
94 | APIHostPorts() [][]instance.HostPort |
95 | + EnvironTag() string |
96 | } |
97 | |
98 | type apiOpenFunc func(*api.Info, api.DialOpts) (apiState, error) |
99 | @@ -212,7 +213,7 @@ |
100 | |
101 | st := val0.(apiState) |
102 | // Even though we are about to update API addresses based on |
103 | - // APIHostPorts in cacheChangedAPIAddresses, we first cache the |
104 | + // APIHostPorts in cacheChangedAPIInfo, we first cache the |
105 | // addresses based on the provider lookup. This is because older API |
106 | // servers didn't return their HostPort information on Login, and we |
107 | // still want to cache our connection information to them. |
108 | @@ -231,7 +232,7 @@ |
109 | } |
110 | } |
111 | // Update API addresses if they've changed. Error is non-fatal. |
112 | - if localerr := cacheChangedAPIAddresses(info, st); localerr != nil { |
113 | + if localerr := cacheChangedAPIInfo(info, st); localerr != nil { |
114 | logger.Warningf("cannot failed to cache API addresses: %v", localerr) |
115 | } |
116 | return st, nil |
117 | @@ -267,11 +268,16 @@ |
118 | return nil, &infoConnectError{fmt.Errorf("no cached addresses")} |
119 | } |
120 | logger.Infof("connecting to API addresses: %v", endpoint.Addresses) |
121 | + environTag := "" |
122 | + if endpoint.EnvironUUID != "" { |
123 | + environTag = names.EnvironTag(endpoint.EnvironUUID) |
124 | + } |
125 | apiInfo := &api.Info{ |
126 | - Addrs: endpoint.Addresses, |
127 | - CACert: endpoint.CACert, |
128 | - Tag: names.UserTag(info.APICredentials().User), |
129 | - Password: info.APICredentials().Password, |
130 | + Addrs: endpoint.Addresses, |
131 | + CACert: endpoint.CACert, |
132 | + Tag: names.UserTag(info.APICredentials().User), |
133 | + Password: info.APICredentials().Password, |
134 | + EnvironTag: environTag, |
135 | } |
136 | st, err := apiOpen(apiInfo, api.DefaultDialOpts()) |
137 | if err != nil { |
138 | @@ -344,9 +350,18 @@ |
139 | // with the provided apiInfo, assuming we've just successfully |
140 | // connected to the API server. |
141 | func cacheAPIInfo(info configstore.EnvironInfo, apiInfo *api.Info) error { |
142 | + environUUID := "" |
143 | + if apiInfo.EnvironTag != "" { |
144 | + var err error |
145 | + _, environUUID, err = names.ParseTag(apiInfo.Tag, names.EnvironTagKind) |
146 | + if err != nil { |
147 | + return fmt.Errorf("invalid API environment tag: %v", err) |
148 | + } |
149 | + } |
150 | info.SetAPIEndpoint(configstore.APIEndpoint{ |
151 | - Addresses: apiInfo.Addrs, |
152 | - CACert: string(apiInfo.CACert), |
153 | + Addresses: apiInfo.Addrs, |
154 | + CACert: string(apiInfo.CACert), |
155 | + EnvironUUID: environUUID, |
156 | }) |
157 | _, username, err := names.ParseTag(apiInfo.Tag, names.UserTagKind) |
158 | if err != nil { |
159 | @@ -359,9 +374,10 @@ |
160 | return info.Write() |
161 | } |
162 | |
163 | -// cacheChangedAPIAddresses updates the local environment settings (.jenv file) |
164 | -// with the provided API server addresses if they have changed. |
165 | -func cacheChangedAPIAddresses(info configstore.EnvironInfo, st apiState) error { |
166 | +// cacheChangedAPIInfo updates the local environment settings (.jenv file) |
167 | +// with the provided API server addresses if they have changed. It will also |
168 | +// save the environment tag if it is available. |
169 | +func cacheChangedAPIInfo(info configstore.EnvironInfo, st apiState) error { |
170 | var addrs []string |
171 | for _, serverHostPorts := range st.APIHostPorts() { |
172 | for _, hostPort := range serverHostPorts { |
173 | @@ -373,11 +389,23 @@ |
174 | } |
175 | } |
176 | endpoint := info.APIEndpoint() |
177 | - if len(addrs) == 0 || !addrsChanged(endpoint.Addresses, addrs) { |
178 | + newEnvironTag := st.EnvironTag() |
179 | + changed := false |
180 | + if newEnvironTag != "" { |
181 | + _, environUUID, err := names.ParseTag(newEnvironTag, names.EnvironTagKind) |
182 | + if err == nil && endpoint.EnvironUUID != environUUID { |
183 | + changed = true |
184 | + endpoint.EnvironUUID = environUUID |
185 | + } |
186 | + } |
187 | + if len(addrs) != 0 && addrsChanged(endpoint.Addresses, addrs) { |
188 | + logger.Debugf("API addresses changed from %q to %q", endpoint.Addresses, addrs) |
189 | + changed = true |
190 | + endpoint.Addresses = addrs |
191 | + } |
192 | + if !changed { |
193 | return nil |
194 | } |
195 | - logger.Debugf("API addresses changed from %q to %q", endpoint.Addresses, addrs) |
196 | - endpoint.Addresses = addrs |
197 | info.SetAPIEndpoint(endpoint) |
198 | if err := info.Write(); err != nil { |
199 | return err |
200 | |
201 | === modified file 'juju/apiconn_test.go' |
202 | --- juju/apiconn_test.go 2014-05-16 01:33:13 +0000 |
203 | +++ juju/apiconn_test.go 2014-06-05 04:28:36 +0000 |
204 | @@ -123,11 +123,13 @@ |
205 | apiHostPorts: [][]instance.HostPort{ |
206 | instance.AddressesWithPort([]instance.Address{instance.NewAddress("0.1.2.3", instance.NetworkUnknown)}, 1234), |
207 | }, |
208 | + environTag: "environment-fake-uuid", |
209 | } |
210 | apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) { |
211 | c.Check(apiInfo.Tag, gc.Equals, "user-foo") |
212 | c.Check(string(apiInfo.CACert), gc.Equals, "certificated") |
213 | c.Check(apiInfo.Password, gc.Equals, "foopass") |
214 | + c.Check(apiInfo.EnvironTag, gc.Equals, "environment-fake-uuid") |
215 | c.Check(opts, gc.DeepEquals, api.DefaultDialOpts()) |
216 | called++ |
217 | return expectState, nil |
218 | @@ -143,7 +145,9 @@ |
219 | c.Assert(mockStore.written, jc.IsTrue) |
220 | info, err := store.ReadInfo("noconfig") |
221 | c.Assert(err, gc.IsNil) |
222 | - c.Assert(info.APIEndpoint().Addresses, gc.DeepEquals, []string{"0.1.2.3:1234"}) |
223 | + ep := info.APIEndpoint() |
224 | + c.Assert(ep.Addresses, gc.DeepEquals, []string{"0.1.2.3:1234"}) |
225 | + c.Check(ep.EnvironUUID, gc.Equals, "fake-uuid") |
226 | mockStore.written = false |
227 | |
228 | // If APIHostPorts haven't changed, then the store won't be updated. |
229 | @@ -185,6 +189,8 @@ |
230 | c.Check(apiInfo.Tag, gc.Equals, "user-admin") |
231 | c.Check(string(apiInfo.CACert), gc.Not(gc.Equals), "") |
232 | c.Check(apiInfo.Password, gc.Equals, "adminpass") |
233 | + // EnvironTag wasn't in regular Config |
234 | + c.Check(apiInfo.EnvironTag, gc.Equals, "") |
235 | c.Check(opts, gc.DeepEquals, api.DefaultDialOpts()) |
236 | called++ |
237 | return expectState, nil |
238 | @@ -202,6 +208,9 @@ |
239 | c.Assert(ep.Addresses, gc.HasLen, 1) |
240 | c.Check(ep.Addresses[0], gc.Matches, `127\.0\.0\.1:\d+`) |
241 | c.Check(ep.CACert, gc.Not(gc.Equals), "") |
242 | + // Old servers won't hand back EnvironTag, so it should stay empty in |
243 | + // the cache |
244 | + c.Check(ep.EnvironUUID, gc.Equals, "") |
245 | creds := info.APICredentials() |
246 | c.Check(creds.User, gc.Equals, "admin") |
247 | c.Check(creds.Password, gc.Equals, "adminpass") |
248 | @@ -227,6 +236,128 @@ |
249 | c.Assert(st, gc.IsNil) |
250 | } |
251 | |
252 | +func (s *NewAPIClientSuite) TestWithInfoNoEnvironTag(c *gc.C) { |
253 | + store := newConfigStore("noconfig", &environInfo{ |
254 | + creds: configstore.APICredentials{ |
255 | + User: "foo", |
256 | + Password: "foopass", |
257 | + }, |
258 | + endpoint: configstore.APIEndpoint{ |
259 | + Addresses: []string{"foo.invalid"}, |
260 | + CACert: "certificated", |
261 | + }, |
262 | + }) |
263 | + |
264 | + called := 0 |
265 | + expectState := &mockAPIState{ |
266 | + apiHostPorts: [][]instance.HostPort{ |
267 | + instance.AddressesWithPort([]instance.Address{instance.NewAddress("0.1.2.3", instance.NetworkUnknown)}, 1234), |
268 | + }, |
269 | + environTag: "environment-fake-uuid", |
270 | + } |
271 | + apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) { |
272 | + c.Check(apiInfo.Tag, gc.Equals, "user-foo") |
273 | + c.Check(string(apiInfo.CACert), gc.Equals, "certificated") |
274 | + c.Check(apiInfo.Password, gc.Equals, "foopass") |
275 | + c.Check(apiInfo.EnvironTag, gc.Equals, "") |
276 | + c.Check(opts, gc.DeepEquals, api.DefaultDialOpts()) |
277 | + called++ |
278 | + return expectState, nil |
279 | + } |
280 | + |
281 | + // Give NewAPIFromStore a store interface that can report when the |
282 | + // config was written to, to check if the cache is updated. |
283 | + mockStore := &storageWithWriteNotify{store: store} |
284 | + st, err := juju.NewAPIFromStore("noconfig", mockStore, apiOpen) |
285 | + c.Assert(err, gc.IsNil) |
286 | + c.Assert(st, gc.Equals, expectState) |
287 | + c.Assert(called, gc.Equals, 1) |
288 | + c.Assert(mockStore.written, jc.IsTrue) |
289 | + info, err := store.ReadInfo("noconfig") |
290 | + c.Assert(err, gc.IsNil) |
291 | + c.Assert(info.APIEndpoint().Addresses, gc.DeepEquals, []string{"0.1.2.3:1234"}) |
292 | + c.Check(info.APIEndpoint().EnvironUUID, gc.Equals, "fake-uuid") |
293 | +} |
294 | + |
295 | +func (s *NewAPIClientSuite) TestWithInfoNoAPIHostports(c *gc.C) { |
296 | + // The local cache doesn't have an EnvironTag, which the API does |
297 | + // return. However, the API doesn't have apiHostPorts, we don't want to |
298 | + // override the local cache with bad endpoints. |
299 | + store := newConfigStore("noconfig", &environInfo{ |
300 | + creds: configstore.APICredentials{ |
301 | + User: "foo", |
302 | + Password: "foopass", |
303 | + }, |
304 | + endpoint: configstore.APIEndpoint{ |
305 | + Addresses: []string{"foo.invalid"}, |
306 | + CACert: "certificated", |
307 | + }, |
308 | + }) |
309 | + |
310 | + called := 0 |
311 | + expectState := &mockAPIState{ |
312 | + apiHostPorts: [][]instance.HostPort{}, |
313 | + environTag: "environment-fake-uuid", |
314 | + } |
315 | + apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) { |
316 | + c.Check(apiInfo.Tag, gc.Equals, "user-foo") |
317 | + c.Check(string(apiInfo.CACert), gc.Equals, "certificated") |
318 | + c.Check(apiInfo.Password, gc.Equals, "foopass") |
319 | + c.Check(apiInfo.EnvironTag, gc.Equals, "") |
320 | + c.Check(opts, gc.DeepEquals, api.DefaultDialOpts()) |
321 | + called++ |
322 | + return expectState, nil |
323 | + } |
324 | + |
325 | + mockStore := &storageWithWriteNotify{store: store} |
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, 1) |
330 | + c.Assert(mockStore.written, jc.IsTrue) |
331 | + info, err := store.ReadInfo("noconfig") |
332 | + c.Assert(err, gc.IsNil) |
333 | + ep := info.APIEndpoint() |
334 | + // We should have cached the environ tag, but not disturbed the |
335 | + // Addresses |
336 | + c.Check(ep.Addresses, gc.HasLen, 1) |
337 | + c.Check(ep.Addresses[0], gc.Matches, `foo\.invalid`) |
338 | + c.Check(ep.EnvironUUID, gc.Equals, "fake-uuid") |
339 | +} |
340 | + |
341 | +func (s *NewAPIClientSuite) TestNoEnvironTagDoesntOverwriteCached(c *gc.C) { |
342 | + store := newConfigStore("noconfig", dummyStoreInfo) |
343 | + called := 0 |
344 | + // State returns a new set of APIHostPorts but not a new EnvironTag. We |
345 | + // shouldn't override the cached value with environ tag of "". |
346 | + expectState := &mockAPIState{ |
347 | + apiHostPorts: [][]instance.HostPort{ |
348 | + instance.AddressesWithPort([]instance.Address{instance.NewAddress("0.1.2.3", instance.NetworkUnknown)}, 1234), |
349 | + }, |
350 | + } |
351 | + apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) { |
352 | + c.Check(apiInfo.Tag, gc.Equals, "user-foo") |
353 | + c.Check(string(apiInfo.CACert), gc.Equals, "certificated") |
354 | + c.Check(apiInfo.Password, gc.Equals, "foopass") |
355 | + c.Check(apiInfo.EnvironTag, gc.Equals, "environment-fake-uuid") |
356 | + c.Check(opts, gc.DeepEquals, api.DefaultDialOpts()) |
357 | + called++ |
358 | + return expectState, nil |
359 | + } |
360 | + |
361 | + mockStore := &storageWithWriteNotify{store: store} |
362 | + st, err := juju.NewAPIFromStore("noconfig", mockStore, apiOpen) |
363 | + c.Assert(err, gc.IsNil) |
364 | + c.Assert(st, gc.Equals, expectState) |
365 | + c.Assert(called, gc.Equals, 1) |
366 | + c.Assert(mockStore.written, jc.IsTrue) |
367 | + info, err := store.ReadInfo("noconfig") |
368 | + c.Assert(err, gc.IsNil) |
369 | + ep := info.APIEndpoint() |
370 | + c.Assert(ep.Addresses, gc.DeepEquals, []string{"0.1.2.3:1234"}) |
371 | + c.Check(ep.EnvironUUID, gc.Equals, "fake-uuid") |
372 | +} |
373 | + |
374 | func (s *NewAPIClientSuite) TestWithInfoAPIOpenError(c *gc.C) { |
375 | store := newConfigStore("noconfig", &environInfo{ |
376 | endpoint: configstore.APIEndpoint{ |
377 | @@ -584,8 +715,9 @@ |
378 | Password: "foopass", |
379 | }, |
380 | endpoint: configstore.APIEndpoint{ |
381 | - Addresses: []string{"foo.invalid"}, |
382 | - CACert: "certificated", |
383 | + Addresses: []string{"foo.invalid"}, |
384 | + CACert: "certificated", |
385 | + EnvironUUID: "fake-uuid", |
386 | }, |
387 | } |
388 | |
389 | @@ -631,12 +763,15 @@ |
390 | apiHostPorts: [][]instance.HostPort{ |
391 | instance.AddressesWithPort([]instance.Address{instance.NewAddress("0.1.2.3", instance.NetworkUnknown)}, 1234), |
392 | }, |
393 | + environTag: "environment-fake-uuid", |
394 | } |
395 | apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) { |
396 | c.Check(apiInfo.Tag, gc.Equals, "user-admin") |
397 | c.Check(string(apiInfo.CACert), gc.Equals, coretesting.CACert) |
398 | c.Check(apiInfo.Password, gc.Equals, coretesting.DefaultMongoPassword) |
399 | c.Check(opts, gc.DeepEquals, api.DefaultDialOpts()) |
400 | + // we didn't know about it when connecting |
401 | + c.Check(apiInfo.EnvironTag, gc.Equals, "") |
402 | called++ |
403 | return expectState, nil |
404 | } |
405 | @@ -644,6 +779,7 @@ |
406 | c.Assert(err, gc.IsNil) |
407 | c.Assert(called, gc.Equals, 1) |
408 | c.Check(endpoint.Addresses, gc.DeepEquals, []string{"0.1.2.3:1234"}) |
409 | + c.Check(endpoint.EnvironUUID, gc.Equals, "fake-uuid") |
410 | } |
411 | |
412 | func (s *APIEndpointForEnvSuite) TestAPIEndpointRefresh(c *gc.C) { |
413 | |
414 | === modified file 'juju/mock_test.go' |
415 | --- juju/mock_test.go 2014-03-31 12:24:52 +0000 |
416 | +++ juju/mock_test.go 2014-06-05 04:28:36 +0000 |
417 | @@ -10,6 +10,7 @@ |
418 | close func(juju.APIState) error |
419 | |
420 | apiHostPorts [][]instance.HostPort |
421 | + environTag string |
422 | } |
423 | |
424 | func (s *mockAPIState) Close() error { |
425 | @@ -23,6 +24,10 @@ |
426 | return s.apiHostPorts |
427 | } |
428 | |
429 | +func (s *mockAPIState) EnvironTag() string { |
430 | + return s.environTag |
431 | +} |
432 | + |
433 | func panicAPIOpen(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) { |
434 | panic("api.Open called unexpectedly") |
435 | } |
436 | |
437 | === modified file 'state/api/apiclient.go' |
438 | --- state/api/apiclient.go 2014-06-02 20:54:43 +0000 |
439 | +++ state/api/apiclient.go 2014-06-05 04:28:36 +0000 |
440 | @@ -16,6 +16,7 @@ |
441 | |
442 | "launchpad.net/juju-core/cert" |
443 | "launchpad.net/juju-core/instance" |
444 | + "launchpad.net/juju-core/names" |
445 | "launchpad.net/juju-core/rpc" |
446 | "launchpad.net/juju-core/rpc/jsoncodec" |
447 | "launchpad.net/juju-core/state/api/params" |
448 | @@ -36,6 +37,9 @@ |
449 | // addr is the address used to connect to the API server. |
450 | addr string |
451 | |
452 | + // environTag holds the environment tag once we're connected |
453 | + environTag string |
454 | + |
455 | // hostPorts is the API server addresses returned from Login, |
456 | // which the client may cache and use for failover. |
457 | hostPorts [][]instance.HostPort |
458 | @@ -50,6 +54,7 @@ |
459 | // tag and password hold the cached login credentials. |
460 | tag string |
461 | password string |
462 | + |
463 | // serverRoot holds the cached API server address and port we used |
464 | // to login, with a https:// prefix. |
465 | serverRoot string |
466 | @@ -81,6 +86,10 @@ |
467 | // Nonce holds the nonce used when provisioning the machine. Used |
468 | // only by the machine agent. |
469 | Nonce string `yaml:",omitempty"` |
470 | + |
471 | + // Environ holds the environ tag for the environment we are trying to |
472 | + // connect to. |
473 | + EnvironTag string |
474 | } |
475 | |
476 | // DialOpts holds configuration parameters that control the |
477 | @@ -120,6 +129,14 @@ |
478 | } |
479 | pool.AddCert(xcert) |
480 | |
481 | + environUUID := "" |
482 | + if info.EnvironTag != "" { |
483 | + _, envUUID, err := names.ParseTag(info.EnvironTag, names.EnvironTagKind) |
484 | + if err != nil { |
485 | + return nil, err |
486 | + } |
487 | + environUUID = envUUID |
488 | + } |
489 | // Dial all addresses at reasonable intervals. |
490 | try := parallel.NewTry(0, nil) |
491 | defer try.Kill() |
492 | @@ -134,7 +151,7 @@ |
493 | addrs = info.Addrs |
494 | } |
495 | for _, addr := range addrs { |
496 | - err := dialWebsocket(addr, opts, pool, try) |
497 | + err := dialWebsocket(addr, environUUID, opts, pool, try) |
498 | if err == parallel.ErrStopped { |
499 | break |
500 | } |
501 | @@ -176,20 +193,32 @@ |
502 | return st, nil |
503 | } |
504 | |
505 | -func dialWebsocket(addr string, opts DialOpts, rootCAs *x509.CertPool, try *parallel.Try) error { |
506 | +func dialWebsocket(addr, environUUID string, opts DialOpts, rootCAs *x509.CertPool, try *parallel.Try) error { |
507 | + cfg, err := setUpWebsocket(addr, environUUID, rootCAs) |
508 | + if err != nil { |
509 | + return err |
510 | + } |
511 | + return try.Start(newWebsocketDialer(cfg, opts)) |
512 | +} |
513 | + |
514 | +func setUpWebsocket(addr, environUUID string, rootCAs *x509.CertPool) (*websocket.Config, error) { |
515 | // origin is required by the WebSocket API, used for "origin policy" |
516 | // in websockets. We pass localhost to satisfy the API; it is |
517 | // inconsequential to us. |
518 | const origin = "http://localhost/" |
519 | - cfg, err := websocket.NewConfig("wss://"+addr+"/", origin) |
520 | + tail := "/" |
521 | + if environUUID != "" { |
522 | + tail = "/" + environUUID + "/api" |
523 | + } |
524 | + cfg, err := websocket.NewConfig("wss://"+addr+tail, origin) |
525 | if err != nil { |
526 | - return err |
527 | + return nil, err |
528 | } |
529 | cfg.TlsConfig = &tls.Config{ |
530 | RootCAs: rootCAs, |
531 | ServerName: "anything", |
532 | } |
533 | - return try.Start(newWebsocketDialer(cfg, opts)) |
534 | + return cfg, nil |
535 | } |
536 | |
537 | // newWebsocketDialer returns a function that |
538 | @@ -215,7 +244,7 @@ |
539 | logger.Debugf("error dialing %q, will retry: %v", cfg.Location, err) |
540 | } else { |
541 | logger.Infof("error dialing %q: %v", cfg.Location, err) |
542 | - return nil, fmt.Errorf("timed out connecting to %q", cfg.Location) |
543 | + return nil, fmt.Errorf("unable to connect to %q", cfg.Location) |
544 | } |
545 | } |
546 | panic("unreachable") |
547 | @@ -272,6 +301,12 @@ |
548 | return s.addr |
549 | } |
550 | |
551 | +// EnvironTag returns the Environment Tag describing the environment we are |
552 | +// connected to. |
553 | +func (s *State) EnvironTag() string { |
554 | + return s.environTag |
555 | +} |
556 | + |
557 | // APIHostPorts returns addresses that may be used to connect |
558 | // to the API server, including the address used to connect. |
559 | // |
560 | |
561 | === modified file 'state/api/apiclient_test.go' |
562 | --- state/api/apiclient_test.go 2014-06-03 12:55:51 +0000 |
563 | +++ state/api/apiclient_test.go 2014-06-05 04:28:36 +0000 |
564 | @@ -13,6 +13,8 @@ |
565 | |
566 | jujutesting "launchpad.net/juju-core/juju/testing" |
567 | "launchpad.net/juju-core/state/api" |
568 | + "launchpad.net/juju-core/state/api/params" |
569 | + coretesting "launchpad.net/juju-core/testing" |
570 | "launchpad.net/juju-core/utils/parallel" |
571 | ) |
572 | |
573 | @@ -22,6 +24,65 @@ |
574 | |
575 | var _ = gc.Suite(&apiclientSuite{}) |
576 | |
577 | +<<<<<<< TREE |
578 | +======= |
579 | +type websocketSuite struct { |
580 | + coretesting.BaseSuite |
581 | +} |
582 | + |
583 | +var _ = gc.Suite(&websocketSuite{}) |
584 | + |
585 | +func (s *apiclientSuite) TestSortLocalhost(c *gc.C) { |
586 | + addrs := []string{ |
587 | + "notlocalhost1", |
588 | + "notlocalhost2", |
589 | + "notlocalhost3", |
590 | + "localhost1", |
591 | + "localhost2", |
592 | + "localhost3", |
593 | + } |
594 | + expectedAddrs := []string{ |
595 | + "localhost1", |
596 | + "localhost2", |
597 | + "localhost3", |
598 | + "notlocalhost1", |
599 | + "notlocalhost2", |
600 | + "notlocalhost3", |
601 | + } |
602 | + var sortedAddrs []string |
603 | + sortedAddrs = append(sortedAddrs, addrs...) |
604 | + sort.Sort(api.LocalFirst(sortedAddrs)) |
605 | + c.Assert(addrs, gc.Not(gc.DeepEquals), sortedAddrs) |
606 | + c.Assert(sortedAddrs, gc.HasLen, 6) |
607 | + c.Assert(sortedAddrs, gc.DeepEquals, expectedAddrs) |
608 | + |
609 | +} |
610 | + |
611 | +func (s *apiclientSuite) TestSortLocalhostIdempotent(c *gc.C) { |
612 | + addrs := []string{ |
613 | + "localhost1", |
614 | + "localhost2", |
615 | + "localhost3", |
616 | + "notlocalhost1", |
617 | + "notlocalhost2", |
618 | + "notlocalhost3", |
619 | + } |
620 | + expectedAddrs := []string{ |
621 | + "localhost1", |
622 | + "localhost2", |
623 | + "localhost3", |
624 | + "notlocalhost1", |
625 | + "notlocalhost2", |
626 | + "notlocalhost3", |
627 | + } |
628 | + var sortedAddrs []string |
629 | + sortedAddrs = append(sortedAddrs, addrs...) |
630 | + sort.Sort(api.LocalFirst(sortedAddrs)) |
631 | + c.Assert(sortedAddrs, gc.DeepEquals, expectedAddrs) |
632 | + |
633 | +} |
634 | + |
635 | +>>>>>>> MERGE-SOURCE |
636 | func (s *apiclientSuite) TestOpenPrefersLocalhostIfPresent(c *gc.C) { |
637 | // Create a socket that proxies to the API server though our localhost address. |
638 | info := s.APIInfo(c) |
639 | @@ -115,7 +176,33 @@ |
640 | addr := listener.Addr().String() |
641 | info.Addrs = []string{addr, addr, addr} |
642 | _, err = api.Open(info, api.DialOpts{}) |
643 | - c.Assert(err, gc.ErrorMatches, `timed out connecting to "wss://.*/"`) |
644 | + c.Assert(err, gc.ErrorMatches, `unable to connect to "wss://.*/"`) |
645 | +} |
646 | + |
647 | +func (s *apiclientSuite) TestOpenPassesEnvironTag(c *gc.C) { |
648 | + info := s.APIInfo(c) |
649 | + env, err := s.State.Environment() |
650 | + c.Assert(err, gc.IsNil) |
651 | + /// TODO: we want to test this eventually, but for now s.APIInfo uses |
652 | + /// conn.StateInfo() which doesn't know about EnvironTag. |
653 | + /// c.Check(info.EnvironTag, gc.Equals, env.Tag()) |
654 | + /// c.Assert(info.EnvironTag, gc.Not(gc.Equals), "") |
655 | + // We start by ensuring we have an invalid tag, and Open should fail. |
656 | + info.EnvironTag = "environment-bad-tag" |
657 | + _, err = api.Open(info, api.DialOpts{}) |
658 | + c.Check(err, gc.ErrorMatches, `unknown environment: "bad-tag"`) |
659 | + c.Check(params.ErrCode(err), gc.Equals, params.CodeNotFound) |
660 | + // Now set it to the right tag, and we should succeed. |
661 | + info.EnvironTag = env.Tag() |
662 | + st, err := api.Open(info, api.DialOpts{}) |
663 | + c.Assert(err, gc.IsNil) |
664 | + st.Close() |
665 | + // Backwards compatibility, we should succeed if we pass just "" as the |
666 | + // environ tag |
667 | + info.EnvironTag = "" |
668 | + st, err = api.Open(info, api.DialOpts{}) |
669 | + c.Assert(err, gc.IsNil) |
670 | + st.Close() |
671 | } |
672 | |
673 | func (s *apiclientSuite) TestDialWebsocketStopped(c *gc.C) { |
674 | @@ -126,3 +213,17 @@ |
675 | c.Assert(err, gc.Equals, parallel.ErrStopped) |
676 | c.Assert(result, gc.IsNil) |
677 | } |
678 | + |
679 | +func (*websocketSuite) TestSetUpWebsocketConfig(c *gc.C) { |
680 | + conf, err := api.SetUpWebsocket("0.1.2.3:1234", "", nil) |
681 | + c.Assert(err, gc.IsNil) |
682 | + c.Check(conf.Location.String(), gc.Equals, "wss://0.1.2.3:1234/") |
683 | + c.Check(conf.Origin.String(), gc.Equals, "http://localhost/") |
684 | +} |
685 | + |
686 | +func (*websocketSuite) TestSetUpWebsocketConfigHandlesEnvironUUID(c *gc.C) { |
687 | + conf, err := api.SetUpWebsocket("0.1.2.3:1234", "dead-beef-1234", nil) |
688 | + c.Assert(err, gc.IsNil) |
689 | + c.Check(conf.Location.String(), gc.Equals, "wss://0.1.2.3:1234/dead-beef-1234/api") |
690 | + c.Check(conf.Origin.String(), gc.Equals, "http://localhost/") |
691 | +} |
692 | |
693 | === modified file 'state/api/client_test.go' |
694 | --- state/api/client_test.go 2014-04-10 09:43:51 +0000 |
695 | +++ state/api/client_test.go 2014-06-05 04:28:36 +0000 |
696 | @@ -162,13 +162,10 @@ |
697 | reader, err := client.WatchDebugLog(params) |
698 | c.Assert(err, gc.IsNil) |
699 | |
700 | - bufReader := bufio.NewReader(reader) |
701 | - location, err := bufReader.ReadString('\n') |
702 | - c.Assert(err, gc.IsNil) |
703 | - connectUrl, err := url.Parse(strings.TrimSpace(location)) |
704 | - c.Assert(err, gc.IsNil) |
705 | + connectURL := connectURLFromReader(c, reader) |
706 | |
707 | - values := connectUrl.Query() |
708 | + c.Assert(connectURL.Path, gc.Matches, "/log") |
709 | + values := connectURL.Query() |
710 | c.Assert(values, jc.DeepEquals, url.Values{ |
711 | "includeEntity": params.IncludeEntity, |
712 | "includeModule": params.IncludeModule, |
713 | @@ -181,6 +178,62 @@ |
714 | }) |
715 | } |
716 | |
717 | +func (s *clientSuite) TestDebugLogRootPath(c *gc.C) { |
718 | + s.PatchValue(api.WebsocketDialConfig, echoURL(c)) |
719 | + |
720 | + // If the server is old, we log at "/log" |
721 | + info := s.APIInfo(c) |
722 | + info.EnvironTag = "" |
723 | + apistate, err := api.Open(info, api.DialOpts{}) |
724 | + c.Assert(err, gc.IsNil) |
725 | + defer apistate.Close() |
726 | + reader, err := apistate.Client().WatchDebugLog(api.DebugLogParams{}) |
727 | + c.Assert(err, gc.IsNil) |
728 | + connectURL := connectURLFromReader(c, reader) |
729 | + c.Assert(connectURL.Path, gc.Matches, "/log") |
730 | +} |
731 | + |
732 | +func (s *clientSuite) TestDebugLogAtUUIDLogPath(c *gc.C) { |
733 | + s.PatchValue(api.WebsocketDialConfig, echoURL(c)) |
734 | + // If the server supports it, we log at "/ENV-UUID/log" |
735 | + environ, err := s.State.Environment() |
736 | + c.Assert(err, gc.IsNil) |
737 | + info := s.APIInfo(c) |
738 | + info.EnvironTag = environ.Tag() |
739 | + apistate, err := api.Open(info, api.DialOpts{}) |
740 | + c.Assert(err, gc.IsNil) |
741 | + defer apistate.Close() |
742 | + reader, err := apistate.Client().WatchDebugLog(api.DebugLogParams{}) |
743 | + c.Assert(err, gc.IsNil) |
744 | + connectURL := connectURLFromReader(c, reader) |
745 | + c.ExpectFailure("debug log always goes to /log for compatibility") |
746 | + c.Assert(connectURL.Path, gc.Matches, fmt.Sprintf("/%s/log", environ.UUID())) |
747 | +} |
748 | + |
749 | +func (s *clientSuite) TestOpenUsesEnvironUUIDPaths(c *gc.C) { |
750 | + info := s.APIInfo(c) |
751 | + // Backwards compatibility, passing EnvironTag = "" should just work |
752 | + info.EnvironTag = "" |
753 | + apistate, err := api.Open(info, api.DialOpts{}) |
754 | + c.Assert(err, gc.IsNil) |
755 | + apistate.Close() |
756 | + |
757 | + // Passing in the correct environment UUID should also work |
758 | + environ, err := s.State.Environment() |
759 | + c.Assert(err, gc.IsNil) |
760 | + info.EnvironTag = environ.Tag() |
761 | + apistate, err = api.Open(info, api.DialOpts{}) |
762 | + c.Assert(err, gc.IsNil) |
763 | + apistate.Close() |
764 | + |
765 | + // Passing in a bad environment UUID should fail with a known error |
766 | + info.EnvironTag = "environment-dead-beef-123456" |
767 | + apistate, err = api.Open(info, api.DialOpts{}) |
768 | + c.Check(err, gc.ErrorMatches, `unknown environment: "dead-beef-123456"`) |
769 | + c.Check(params.ErrCode(err), gc.Equals, params.CodeNotFound) |
770 | + c.Assert(apistate, gc.IsNil) |
771 | +} |
772 | + |
773 | // badReader raises err when Read is called. |
774 | type badReader struct { |
775 | err error |
776 | @@ -203,3 +256,13 @@ |
777 | return pr, nil |
778 | } |
779 | } |
780 | + |
781 | +func connectURLFromReader(c *gc.C, rc io.ReadCloser) *url.URL { |
782 | + bufReader := bufio.NewReader(rc) |
783 | + location, err := bufReader.ReadString('\n') |
784 | + c.Assert(err, gc.IsNil) |
785 | + connectURL, err := url.Parse(strings.TrimSpace(location)) |
786 | + c.Assert(err, gc.IsNil) |
787 | + rc.Close() |
788 | + return connectURL |
789 | +} |
790 | |
791 | === modified file 'state/api/export_test.go' |
792 | --- state/api/export_test.go 2014-04-16 12:55:56 +0000 |
793 | +++ state/api/export_test.go 2014-06-05 04:28:36 +0000 |
794 | @@ -7,6 +7,7 @@ |
795 | NewWebsocketDialer = newWebsocketDialer |
796 | |
797 | WebsocketDialConfig = &websocketDialConfig |
798 | + SetUpWebsocket = setUpWebsocket |
799 | SlideAddressToFront = slideAddressToFront |
800 | ) |
801 | |
802 | |
803 | === modified file 'state/api/params/params.go' |
804 | --- state/api/params/params.go 2014-05-27 08:30:44 +0000 |
805 | +++ state/api/params/params.go 2014-06-05 04:28:36 +0000 |
806 | @@ -719,7 +719,8 @@ |
807 | |
808 | // LoginResult holds the result of a Login call. |
809 | type LoginResult struct { |
810 | - Servers [][]instance.HostPort |
811 | + Servers [][]instance.HostPort |
812 | + EnvironTag string |
813 | } |
814 | |
815 | // EnsureAvailability contains arguments for |
816 | |
817 | === modified file 'state/api/state.go' |
818 | --- state/api/state.go 2014-04-16 12:55:56 +0000 |
819 | +++ state/api/state.go 2014-06-05 04:28:36 +0000 |
820 | @@ -42,6 +42,7 @@ |
821 | return err |
822 | } |
823 | st.hostPorts = hostPorts |
824 | + st.environTag = result.EnvironTag |
825 | } |
826 | return err |
827 | } |
828 | |
829 | === modified file 'state/api/state_test.go' |
830 | --- state/api/state_test.go 2014-05-20 04:27:02 +0000 |
831 | +++ state/api/state_test.go 2014-06-05 04:28:36 +0000 |
832 | @@ -54,10 +54,30 @@ |
833 | s.State.SetAPIHostPorts([][]instance.HostPort{badServer}) |
834 | apistate, err := api.Open(info, api.DialOpts{}) |
835 | c.Assert(err, gc.IsNil) |
836 | + defer apistate.Close() |
837 | hostports := apistate.APIHostPorts() |
838 | c.Check(hostports, gc.DeepEquals, [][]instance.HostPort{serverhostports, badServer}) |
839 | } |
840 | |
841 | +func (s *stateSuite) TestLoginSetsEnvironTag(c *gc.C) { |
842 | + env, err := s.State.Environment() |
843 | + c.Assert(err, gc.IsNil) |
844 | + info := s.APIInfo(c) |
845 | + tag := info.Tag |
846 | + password := info.Password |
847 | + info.Tag = "" |
848 | + info.Password = "" |
849 | + apistate, err := api.Open(info, api.DialOpts{}) |
850 | + c.Assert(err, gc.IsNil) |
851 | + defer apistate.Close() |
852 | + // We haven't called Login yet, so the EnvironTag shouldn't be set. |
853 | + c.Check(apistate.EnvironTag(), gc.Equals, "") |
854 | + err = apistate.Login(tag, password, "") |
855 | + c.Assert(err, gc.IsNil) |
856 | + // Now that we've logged in, EnvironTag should be updated correctly. |
857 | + c.Check(apistate.EnvironTag(), gc.Equals, env.Tag()) |
858 | +} |
859 | + |
860 | func (s *stateSuite) TestAPIHostPortsMovesConnectedValueFirst(c *gc.C) { |
861 | hostportslist := s.APIState.APIHostPorts() |
862 | c.Check(hostportslist, gc.HasLen, 1) |
863 | @@ -91,6 +111,7 @@ |
864 | s.State.SetAPIHostPorts(current) |
865 | apistate, err := api.Open(info, api.DialOpts{}) |
866 | c.Assert(err, gc.IsNil) |
867 | + defer apistate.Close() |
868 | hostports := apistate.APIHostPorts() |
869 | // We should have rotate the server we connected to as the first item, |
870 | // and the address of that server as the first address |
871 | |
872 | === modified file 'state/apiserver/admin.go' |
873 | --- state/apiserver/admin.go 2014-05-20 08:02:01 +0000 |
874 | +++ state/apiserver/admin.go 2014-06-05 04:28:36 +0000 |
875 | @@ -105,8 +105,16 @@ |
876 | } |
877 | logger.Debugf("hostPorts: %v", hostPorts) |
878 | |
879 | + environ, err := a.root.srv.state.Environment() |
880 | + if err != nil { |
881 | + return params.LoginResult{}, err |
882 | + } |
883 | + |
884 | a.root.rpcConn.Serve(newRoot, serverError) |
885 | - return params.LoginResult{hostPorts}, nil |
886 | + return params.LoginResult{ |
887 | + Servers: hostPorts, |
888 | + EnvironTag: environ.Tag(), |
889 | + }, nil |
890 | } |
891 | |
892 | var doCheckCreds = checkCreds |
893 | @@ -185,3 +193,15 @@ |
894 | newRoot.resources.RegisterNamed("pingTimeout", pingTimeout) |
895 | return nil |
896 | } |
897 | + |
898 | +// errRoot implements the API that a client first sees |
899 | +// when connecting to the API. It exposes the same API as initialRoot, except |
900 | +// it returns the requested error when the client makes any request. |
901 | +type errRoot struct { |
902 | + err error |
903 | +} |
904 | + |
905 | +// Admin conforms to the same API as initialRoot, but we'll always return (nil, err) |
906 | +func (r *errRoot) Admin(id string) (*srvAdmin, error) { |
907 | + return nil, r.err |
908 | +} |
909 | |
910 | === modified file 'state/apiserver/apiserver.go' |
911 | --- state/apiserver/apiserver.go 2014-04-30 23:18:40 +0000 |
912 | +++ state/apiserver/apiserver.go 2014-06-05 04:28:36 +0000 |
913 | @@ -13,6 +13,7 @@ |
914 | "time" |
915 | |
916 | "code.google.com/p/go.net/websocket" |
917 | + "github.com/bmizerany/pat" |
918 | "github.com/juju/loggo" |
919 | "launchpad.net/tomb" |
920 | |
921 | @@ -31,13 +32,14 @@ |
922 | |
923 | // Server holds the server side of the API. |
924 | type Server struct { |
925 | - tomb tomb.Tomb |
926 | - wg sync.WaitGroup |
927 | - state *state.State |
928 | - addr net.Addr |
929 | - dataDir string |
930 | - logDir string |
931 | - limiter utils.Limiter |
932 | + tomb tomb.Tomb |
933 | + wg sync.WaitGroup |
934 | + state *state.State |
935 | + environUUID string |
936 | + addr net.Addr |
937 | + dataDir string |
938 | + logDir string |
939 | + limiter utils.Limiter |
940 | } |
941 | |
942 | // NewServer serves the given state by accepting requests on the given |
943 | @@ -151,6 +153,15 @@ |
944 | func (n requestNotifier) ClientReply(req rpc.Request, hdr *rpc.Header, body interface{}) { |
945 | } |
946 | |
947 | +func handleAll(mux *pat.PatternServeMux, pattern string, handler http.Handler) { |
948 | + mux.Get(pattern, handler) |
949 | + mux.Post(pattern, handler) |
950 | + mux.Head(pattern, handler) |
951 | + mux.Put(pattern, handler) |
952 | + mux.Del(pattern, handler) |
953 | + mux.Options(pattern, handler) |
954 | +} |
955 | + |
956 | func (srv *Server) run(lis net.Listener) { |
957 | defer srv.tomb.Done() |
958 | defer srv.wg.Wait() // wait for any outstanding requests to complete. |
959 | @@ -166,18 +177,38 @@ |
960 | srv.tomb.Kill(err) |
961 | srv.wg.Done() |
962 | }() |
963 | - mux := http.NewServeMux() |
964 | - mux.HandleFunc("/", srv.apiHandler) |
965 | - mux.Handle("/log", |
966 | - &debugLogHandler{ |
967 | - httpHandler: httpHandler{state: srv.state}, |
968 | - logDir: srv.logDir}) |
969 | - mux.Handle("/charms", |
970 | - &charmsHandler{ |
971 | - httpHandler: httpHandler{state: srv.state}, |
972 | - dataDir: srv.dataDir}) |
973 | - mux.Handle("/tools", |
974 | - &toolsHandler{httpHandler{state: srv.state}}) |
975 | + // for pat based handlers, they are matched in-order of being |
976 | + // registered, with first match wins. So more specific ones have to be |
977 | + // registered first. |
978 | + mux := pat.New() |
979 | + // For backwards compatibility we register all the old paths |
980 | + handleAll(mux, "/:envuuid/log", |
981 | + &debugLogHandler{ |
982 | + httpHandler: httpHandler{state: srv.state}, |
983 | + logDir: srv.logDir}) |
984 | + handleAll(mux, "/:envuuid/charms", |
985 | + &charmsHandler{ |
986 | + httpHandler: httpHandler{state: srv.state}, |
987 | + dataDir: srv.dataDir}) |
988 | + // TODO: We can switch from handleAll to mux.Post/Get/etc for entries |
989 | + // where we only want to support specific request methods. However, our |
990 | + // tests currently assert that errors come back as application/json and |
991 | + // pat only does "text/plain" responses. |
992 | + handleAll(mux, "/:envuuid/tools", |
993 | + &toolsHandler{httpHandler{state: srv.state}}) |
994 | + handleAll(mux, "/:envuuid/api", http.HandlerFunc(srv.apiHandler)) |
995 | + // For backwards compatibility we register all the old paths |
996 | + handleAll(mux, "/log", |
997 | + &debugLogHandler{ |
998 | + httpHandler: httpHandler{state: srv.state}, |
999 | + logDir: srv.logDir}) |
1000 | + handleAll(mux, "/charms", |
1001 | + &charmsHandler{ |
1002 | + httpHandler: httpHandler{state: srv.state}, |
1003 | + dataDir: srv.dataDir}) |
1004 | + handleAll(mux, "/tools", |
1005 | + &toolsHandler{httpHandler{state: srv.state}}) |
1006 | + handleAll(mux, "/", http.HandlerFunc(srv.apiHandler)) |
1007 | // The error from http.Serve is not interesting. |
1008 | http.Serve(lis, mux) |
1009 | } |
1010 | @@ -197,7 +228,9 @@ |
1011 | if srv.tomb.Err() != tomb.ErrStillAlive { |
1012 | return |
1013 | } |
1014 | - if err := srv.serveConn(conn, reqNotifier); err != nil { |
1015 | + envUUID := req.URL.Query().Get(":envuuid") |
1016 | + logger.Tracef("got a request for env %q", envUUID) |
1017 | + if err := srv.serveConn(conn, reqNotifier, envUUID); err != nil { |
1018 | logger.Errorf("error serving RPCs: %v", err) |
1019 | } |
1020 | }, |
1021 | @@ -210,7 +243,31 @@ |
1022 | return srv.addr.String() |
1023 | } |
1024 | |
1025 | -func (srv *Server) serveConn(wsConn *websocket.Conn, reqNotifier *requestNotifier) error { |
1026 | +func (srv *Server) validateEnvironUUID(envUUID string) error { |
1027 | + if envUUID == "" { |
1028 | + // We allow the environUUID to be empty for 2 cases |
1029 | + // 1) Compatibility with older clients |
1030 | + // 2) On first connect. The environment UUID is currently |
1031 | + // generated by 'jujud bootstrap-state', and we haven't |
1032 | + // threaded that information all the way back to the 'juju |
1033 | + // bootstrap' process to be able to cache the value until |
1034 | + // after we've connected one time. |
1035 | + return nil |
1036 | + } |
1037 | + if srv.environUUID == "" { |
1038 | + env, err := srv.state.Environment() |
1039 | + if err != nil { |
1040 | + return err |
1041 | + } |
1042 | + srv.environUUID = env.UUID() |
1043 | + } |
1044 | + if envUUID != srv.environUUID { |
1045 | + return common.UnknownEnvironmentError(envUUID) |
1046 | + } |
1047 | + return nil |
1048 | +} |
1049 | + |
1050 | +func (srv *Server) serveConn(wsConn *websocket.Conn, reqNotifier *requestNotifier, envUUID string) error { |
1051 | codec := jsoncodec.NewWebsocket(wsConn) |
1052 | if loggo.GetLogger("juju.rpc.jsoncodec").EffectiveLogLevel() <= loggo.TRACE { |
1053 | codec.SetLogging(true) |
1054 | @@ -222,7 +279,12 @@ |
1055 | notifier = reqNotifier |
1056 | } |
1057 | conn := rpc.NewConn(codec, notifier) |
1058 | - conn.Serve(newStateServer(srv, conn, reqNotifier, srv.limiter), serverError) |
1059 | + err := srv.validateEnvironUUID(envUUID) |
1060 | + if err != nil { |
1061 | + conn.Serve(&errRoot{err}, serverError) |
1062 | + } else { |
1063 | + conn.Serve(newStateServer(srv, conn, reqNotifier, srv.limiter), serverError) |
1064 | + } |
1065 | conn.Start() |
1066 | select { |
1067 | case <-conn.Dead(): |
1068 | |
1069 | === modified file 'state/apiserver/charms.go' |
1070 | --- state/apiserver/charms.go 2014-05-13 04:50:10 +0000 |
1071 | +++ state/apiserver/charms.go 2014-06-05 04:28:36 +0000 |
1072 | @@ -44,6 +44,10 @@ |
1073 | h.authError(w, h) |
1074 | return |
1075 | } |
1076 | + if err := h.validateEnvironUUID(r); err != nil { |
1077 | + h.sendError(w, http.StatusNotFound, err.Error()) |
1078 | + return |
1079 | + } |
1080 | |
1081 | switch r.Method { |
1082 | case "POST": |
1083 | |
1084 | === modified file 'state/apiserver/charms_test.go' |
1085 | --- state/apiserver/charms_test.go 2014-03-13 23:30:56 +0000 |
1086 | +++ state/apiserver/charms_test.go 2014-06-05 04:28:36 +0000 |
1087 | @@ -56,6 +56,16 @@ |
1088 | return utils.GetNonValidatingHTTPClient().Do(req) |
1089 | } |
1090 | |
1091 | +func (s *authHttpSuite) baseURL(c *gc.C) *url.URL { |
1092 | + _, info, err := s.APIConn.Environ.StateInfo() |
1093 | + c.Assert(err, gc.IsNil) |
1094 | + return &url.URL{ |
1095 | + Scheme: "https", |
1096 | + Host: info.Addrs[0], |
1097 | + Path: "", |
1098 | + } |
1099 | +} |
1100 | + |
1101 | func (s *authHttpSuite) authRequest(c *gc.C, method, uri, contentType string, body io.Reader) (*http.Response, error) { |
1102 | return s.sendRequest(c, s.userTag, s.password, method, uri, contentType, body) |
1103 | } |
1104 | @@ -226,6 +236,40 @@ |
1105 | c.Assert(downloadedSHA256, gc.Equals, expectedSHA256) |
1106 | } |
1107 | |
1108 | +func (s *charmsSuite) TestUploadAllowsTopLevelPath(c *gc.C) { |
1109 | + ch := coretesting.Charms.Bundle(c.MkDir(), "dummy") |
1110 | + // Backwards compatibility check, that we can upload charms to |
1111 | + // https://host:port/charms |
1112 | + url := s.charmsURL(c, "series=quantal") |
1113 | + url.Path = "/charms" |
1114 | + resp, err := s.uploadRequest(c, url.String(), true, ch.Path) |
1115 | + c.Assert(err, gc.IsNil) |
1116 | + expectedURL := charm.MustParseURL("local:quantal/dummy-1") |
1117 | + s.assertUploadResponse(c, resp, expectedURL.String()) |
1118 | +} |
1119 | + |
1120 | +func (s *charmsSuite) TestUploadAllowsEnvUUIDPath(c *gc.C) { |
1121 | + // Check that we can upload charms to https://host:port/ENVUUID/charms |
1122 | + ch := coretesting.Charms.Bundle(c.MkDir(), "dummy") |
1123 | + environ, err := s.State.Environment() |
1124 | + c.Assert(err, gc.IsNil) |
1125 | + url := s.charmsURL(c, "series=quantal") |
1126 | + url.Path = fmt.Sprintf("/%s/charms", environ.UUID()) |
1127 | + resp, err := s.uploadRequest(c, url.String(), true, ch.Path) |
1128 | + c.Assert(err, gc.IsNil) |
1129 | + expectedURL := charm.MustParseURL("local:quantal/dummy-1") |
1130 | + s.assertUploadResponse(c, resp, expectedURL.String()) |
1131 | +} |
1132 | + |
1133 | +func (s *charmsSuite) TestUploadRejectsWrongEnvUUIDPath(c *gc.C) { |
1134 | + // Check that we cannot upload charms to https://host:port/BADENVUUID/charms |
1135 | + url := s.charmsURL(c, "series=quantal") |
1136 | + url.Path = "/dead-beef-123456/charms" |
1137 | + resp, err := s.authRequest(c, "POST", url.String(), "", nil) |
1138 | + c.Assert(err, gc.IsNil) |
1139 | + s.assertErrorResponse(c, resp, http.StatusNotFound, `unknown environment: "dead-beef-123456"`) |
1140 | +} |
1141 | + |
1142 | func (s *charmsSuite) TestUploadRepackagesNestedArchives(c *gc.C) { |
1143 | // Make a clone of the dummy charm in a nested directory. |
1144 | rootDir := c.MkDir() |
1145 | @@ -370,6 +414,44 @@ |
1146 | } |
1147 | } |
1148 | |
1149 | +func (s *charmsSuite) TestGetAllowsTopLevelPath(c *gc.C) { |
1150 | + ch := coretesting.Charms.Bundle(c.MkDir(), "dummy") |
1151 | + _, err := s.uploadRequest( |
1152 | + c, s.charmsURI(c, "?series=quantal"), true, ch.Path) |
1153 | + c.Assert(err, gc.IsNil) |
1154 | + // Backwards compatibility check, that we can GET from charms at |
1155 | + // https://host:port/charms |
1156 | + url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision") |
1157 | + url.Path = "/charms" |
1158 | + resp, err := s.authRequest(c, "GET", url.String(), "", nil) |
1159 | + c.Assert(err, gc.IsNil) |
1160 | + s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8") |
1161 | +} |
1162 | + |
1163 | +func (s *charmsSuite) TestGetAllowsEnvUUIDPath(c *gc.C) { |
1164 | + ch := coretesting.Charms.Bundle(c.MkDir(), "dummy") |
1165 | + _, err := s.uploadRequest( |
1166 | + c, s.charmsURI(c, "?series=quantal"), true, ch.Path) |
1167 | + c.Assert(err, gc.IsNil) |
1168 | + // Check that we can GET from charms at https://host:port/ENVUUID/charms |
1169 | + environ, err := s.State.Environment() |
1170 | + c.Assert(err, gc.IsNil) |
1171 | + url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision") |
1172 | + url.Path = fmt.Sprintf("/%s/charms", environ.UUID()) |
1173 | + resp, err := s.authRequest(c, "GET", url.String(), "", nil) |
1174 | + c.Assert(err, gc.IsNil) |
1175 | + s.assertGetFileResponse(c, resp, "1", "text/plain; charset=utf-8") |
1176 | +} |
1177 | + |
1178 | +func (s *charmsSuite) TestGetRejectsWrongEnvUUIDPath(c *gc.C) { |
1179 | + // Check that we cannot upload charms to https://host:port/BADENVUUID/charms |
1180 | + url := s.charmsURL(c, "url=local:quantal/dummy-1&file=revision") |
1181 | + url.Path = "/dead-beef-123456/charms" |
1182 | + resp, err := s.authRequest(c, "GET", url.String(), "", nil) |
1183 | + c.Assert(err, gc.IsNil) |
1184 | + s.assertErrorResponse(c, resp, http.StatusNotFound, `unknown environment: "dead-beef-123456"`) |
1185 | +} |
1186 | + |
1187 | func (s *charmsSuite) TestGetReturnsManifest(c *gc.C) { |
1188 | // Add the dummy charm. |
1189 | ch := coretesting.Charms.Bundle(c.MkDir(), "dummy") |
1190 | @@ -416,10 +498,18 @@ |
1191 | s.assertGetFileResponse(c, resp, contents, "application/javascript") |
1192 | } |
1193 | |
1194 | +func (s *charmsSuite) charmsURL(c *gc.C, query string) *url.URL { |
1195 | + uri := s.baseURL(c) |
1196 | + uri.Path += "/charms" |
1197 | + uri.RawQuery = query |
1198 | + return uri |
1199 | +} |
1200 | + |
1201 | func (s *charmsSuite) charmsURI(c *gc.C, query string) string { |
1202 | - _, info, err := s.APIConn.Environ.StateInfo() |
1203 | - c.Assert(err, gc.IsNil) |
1204 | - return "https://" + info.Addrs[0] + "/charms" + query |
1205 | + if query != "" && query[0] == '?' { |
1206 | + query = query[1:] |
1207 | + } |
1208 | + return s.charmsURL(c, query).String() |
1209 | } |
1210 | |
1211 | func (s *charmsSuite) assertUploadResponse(c *gc.C, resp *http.Response, expCharmURL string) { |
1212 | |
1213 | === modified file 'state/apiserver/common/errors.go' |
1214 | --- state/apiserver/common/errors.go 2014-05-13 04:30:48 +0000 |
1215 | +++ state/apiserver/common/errors.go 2014-06-05 04:28:36 +0000 |
1216 | @@ -44,6 +44,23 @@ |
1217 | return ok |
1218 | } |
1219 | |
1220 | +type unknownEnvironmentError struct { |
1221 | + uuid string |
1222 | +} |
1223 | + |
1224 | +func (e *unknownEnvironmentError) Error() string { |
1225 | + return fmt.Sprintf("unknown environment: %q", e.uuid) |
1226 | +} |
1227 | + |
1228 | +func UnknownEnvironmentError(uuid string) error { |
1229 | + return &unknownEnvironmentError{uuid: uuid} |
1230 | +} |
1231 | + |
1232 | +func IsUnknownEnviromentError(err error) bool { |
1233 | + _, ok := err.(*unknownEnvironmentError) |
1234 | + return ok |
1235 | +} |
1236 | + |
1237 | var ( |
1238 | ErrBadId = stderrors.New("id not found") |
1239 | ErrBadCreds = stderrors.New("invalid entity name or password") |
1240 | @@ -105,6 +122,8 @@ |
1241 | code = params.CodeNoAddressSet |
1242 | case state.IsNotProvisionedError(err): |
1243 | code = params.CodeNotProvisioned |
1244 | + case IsUnknownEnviromentError(err): |
1245 | + code = params.CodeNotFound |
1246 | default: |
1247 | code = params.ErrCode(err) |
1248 | } |
1249 | |
1250 | === modified file 'state/apiserver/common/errors_test.go' |
1251 | --- state/apiserver/common/errors_test.go 2014-05-20 04:27:02 +0000 |
1252 | +++ state/apiserver/common/errors_test.go 2014-06-05 04:28:36 +0000 |
1253 | @@ -105,6 +105,10 @@ |
1254 | err: unhashableError{"foo"}, |
1255 | code: "", |
1256 | }, { |
1257 | + err: common.UnknownEnvironmentError("dead-beef-123456"), |
1258 | + code: params.CodeNotFound, |
1259 | + helperFunc: params.IsCodeNotFound, |
1260 | +}, { |
1261 | err: nil, |
1262 | code: "", |
1263 | }} |
1264 | @@ -129,3 +133,8 @@ |
1265 | } |
1266 | } |
1267 | } |
1268 | + |
1269 | +func (s *errorsSuite) TestUnknownEnvironment(c *gc.C) { |
1270 | + err := common.UnknownEnvironmentError("dead-beef") |
1271 | + c.Check(err, gc.ErrorMatches, `unknown environment: "dead-beef"`) |
1272 | +} |
1273 | |
1274 | === modified file 'state/apiserver/debuglog.go' |
1275 | --- state/apiserver/debuglog.go 2014-04-14 04:13:51 +0000 |
1276 | +++ state/apiserver/debuglog.go 2014-06-05 04:28:36 +0000 |
1277 | @@ -55,6 +55,11 @@ |
1278 | socket.Close() |
1279 | return |
1280 | } |
1281 | + if err := h.validateEnvironUUID(req); err != nil { |
1282 | + h.sendError(socket, err) |
1283 | + socket.Close() |
1284 | + return |
1285 | + } |
1286 | stream, err := newLogStream(req.URL.Query()) |
1287 | if err != nil { |
1288 | h.sendError(socket, err) |
1289 | |
1290 | === modified file 'state/apiserver/debuglog_test.go' |
1291 | --- state/apiserver/debuglog_test.go 2014-04-11 01:45:10 +0000 |
1292 | +++ state/apiserver/debuglog_test.go 2014-06-05 04:28:36 +0000 |
1293 | @@ -8,6 +8,7 @@ |
1294 | "crypto/tls" |
1295 | "crypto/x509" |
1296 | "encoding/json" |
1297 | + "fmt" |
1298 | "io" |
1299 | "net/http" |
1300 | "net/url" |
1301 | @@ -67,14 +68,43 @@ |
1302 | s.assertWebsocketClosed(c, reader) |
1303 | } |
1304 | |
1305 | +func (s *debugLogSuite) assertLogReader(c *gc.C, reader *bufio.Reader) { |
1306 | + s.assertLogFollowing(c, reader) |
1307 | + s.writeLogLines(c, logLineCount) |
1308 | + |
1309 | + linesRead := s.readLogLines(c, reader, logLineCount) |
1310 | + c.Assert(linesRead, jc.DeepEquals, logLines) |
1311 | +} |
1312 | + |
1313 | func (s *debugLogSuite) TestServesLog(c *gc.C) { |
1314 | s.ensureLogFile(c) |
1315 | reader := s.openWebsocket(c, nil) |
1316 | - s.assertLogFollowing(c, reader) |
1317 | - s.writeLogLines(c, logLineCount) |
1318 | - |
1319 | - linesRead := s.readLogLines(c, reader, logLineCount) |
1320 | - c.Assert(linesRead, jc.DeepEquals, logLines) |
1321 | + s.assertLogReader(c, reader) |
1322 | +} |
1323 | + |
1324 | +func (s *debugLogSuite) TestReadFromTopLevelPath(c *gc.C) { |
1325 | + // Backwards compatibility check, that we can read the log file at |
1326 | + // https://host:port/log |
1327 | + s.ensureLogFile(c) |
1328 | + reader := s.openWebsocketCustomPath(c, "/log") |
1329 | + s.assertLogReader(c, reader) |
1330 | +} |
1331 | + |
1332 | +func (s *debugLogSuite) TestReadFromEnvUUIDPath(c *gc.C) { |
1333 | + // Check that we can read the log at https://host:port/ENVUUID/log |
1334 | + environ, err := s.State.Environment() |
1335 | + c.Assert(err, gc.IsNil) |
1336 | + s.ensureLogFile(c) |
1337 | + reader := s.openWebsocketCustomPath(c, fmt.Sprintf("/%s/log", environ.UUID())) |
1338 | + s.assertLogReader(c, reader) |
1339 | +} |
1340 | + |
1341 | +func (s *debugLogSuite) TestReadRejectsWrongEnvUUIDPath(c *gc.C) { |
1342 | + // Check that we cannot upload charms to https://host:port/BADENVUUID/charms |
1343 | + s.ensureLogFile(c) |
1344 | + reader := s.openWebsocketCustomPath(c, "/dead-beef-123456/log") |
1345 | + s.assertErrorResponse(c, reader, `unknown environment: "dead-beef-123456"`) |
1346 | + s.assertWebsocketClosed(c, reader) |
1347 | } |
1348 | |
1349 | func (s *debugLogSuite) TestReadsFromEnd(c *gc.C) { |
1350 | @@ -167,6 +197,16 @@ |
1351 | return bufio.NewReader(conn) |
1352 | } |
1353 | |
1354 | +func (s *debugLogSuite) openWebsocketCustomPath(c *gc.C, path string) *bufio.Reader { |
1355 | + server := s.logURL(c, "wss", nil) |
1356 | + server.Path = path |
1357 | + header := utils.BasicAuthHeader(s.userTag, s.password) |
1358 | + conn, err := s.dialWebsocketFromURL(c, server.String(), header) |
1359 | + c.Assert(err, gc.IsNil) |
1360 | + s.AddCleanup(func(_ *gc.C) { conn.Close() }) |
1361 | + return bufio.NewReader(conn) |
1362 | +} |
1363 | + |
1364 | func (s *debugLogSuite) ensureLogFile(c *gc.C) { |
1365 | if s.logFile != nil { |
1366 | return |
1367 | @@ -192,6 +232,10 @@ |
1368 | |
1369 | func (s *debugLogSuite) dialWebsocketInternal(c *gc.C, queryParams url.Values, header http.Header) (*websocket.Conn, error) { |
1370 | server := s.logURL(c, "wss", queryParams).String() |
1371 | + return s.dialWebsocketFromURL(c, server, header) |
1372 | +} |
1373 | + |
1374 | +func (s *debugLogSuite) dialWebsocketFromURL(c *gc.C, server string, header http.Header) (*websocket.Conn, error) { |
1375 | c.Logf("dialing %v", server) |
1376 | config, err := websocket.NewConfig(server, "http://localhost/") |
1377 | c.Assert(err, gc.IsNil) |
1378 | @@ -208,18 +252,15 @@ |
1379 | } |
1380 | |
1381 | func (s *debugLogSuite) logURL(c *gc.C, scheme string, queryParams url.Values) *url.URL { |
1382 | - _, info, err := s.APIConn.Environ.StateInfo() |
1383 | - c.Assert(err, gc.IsNil) |
1384 | + logURL := s.baseURL(c) |
1385 | query := "" |
1386 | if queryParams != nil { |
1387 | query = queryParams.Encode() |
1388 | } |
1389 | - return &url.URL{ |
1390 | - Scheme: scheme, |
1391 | - Host: info.Addrs[0], |
1392 | - Path: "/log", |
1393 | - RawQuery: query, |
1394 | - } |
1395 | + logURL.Scheme = scheme |
1396 | + logURL.Path += "/log" |
1397 | + logURL.RawQuery = query |
1398 | + return logURL |
1399 | } |
1400 | |
1401 | func (s *debugLogSuite) assertWebsocketClosed(c *gc.C, reader *bufio.Reader) { |
1402 | |
1403 | === modified file 'state/apiserver/export_test.go' |
1404 | --- state/apiserver/export_test.go 2014-04-04 15:22:39 +0000 |
1405 | +++ state/apiserver/export_test.go 2014-06-05 04:28:36 +0000 |
1406 | @@ -36,3 +36,7 @@ |
1407 | doCheckCreds = delayedCheckCreds |
1408 | return |
1409 | } |
1410 | + |
1411 | +func NewErrRoot(err error) *errRoot { |
1412 | + return &errRoot{err} |
1413 | +} |
1414 | |
1415 | === modified file 'state/apiserver/httphandler.go' |
1416 | --- state/apiserver/httphandler.go 2014-04-07 04:50:31 +0000 |
1417 | +++ state/apiserver/httphandler.go 2014-06-05 04:28:36 +0000 |
1418 | @@ -56,6 +56,32 @@ |
1419 | return err |
1420 | } |
1421 | |
1422 | +func (h *httpHandler) getEnvironUUID(r *http.Request) string { |
1423 | + return r.URL.Query().Get(":envuuid") |
1424 | +} |
1425 | + |
1426 | +func (h *httpHandler) validateEnvironUUID(r *http.Request) error { |
1427 | + // Note: this is only true until we have support for multiple |
1428 | + // environments. For now, there is only one, so we make sure that is |
1429 | + // the one being addressed. |
1430 | + envUUID := h.getEnvironUUID(r) |
1431 | + logger.Tracef("got a request for env %q", envUUID) |
1432 | + if envUUID == "" { |
1433 | + return nil |
1434 | + } |
1435 | + env, err := h.state.Environment() |
1436 | + if err != nil { |
1437 | + logger.Infof("error looking up environment: %v", err) |
1438 | + return err |
1439 | + } |
1440 | + if env.UUID() != envUUID { |
1441 | + logger.Infof("environment uuid mismatch: %v != %v", |
1442 | + envUUID, env.UUID()) |
1443 | + return common.UnknownEnvironmentError(envUUID) |
1444 | + } |
1445 | + return nil |
1446 | +} |
1447 | + |
1448 | // authError sends an unauthorized error. |
1449 | func (h *httpHandler) authError(w http.ResponseWriter, sender errorSender) { |
1450 | w.Header().Set("WWW-Authenticate", `Basic realm="juju"`) |
1451 | |
1452 | === modified file 'state/apiserver/login_test.go' |
1453 | --- state/apiserver/login_test.go 2014-04-17 13:14:49 +0000 |
1454 | +++ state/apiserver/login_test.go 2014-06-05 04:28:36 +0000 |
1455 | @@ -59,11 +59,14 @@ |
1456 | "", "", |
1457 | ) |
1458 | c.Assert(err, gc.IsNil) |
1459 | + env, err := s.State.Environment() |
1460 | + c.Assert(err, gc.IsNil) |
1461 | info := &api.Info{ |
1462 | - Tag: "", |
1463 | - Password: "", |
1464 | - Addrs: []string{srv.Addr()}, |
1465 | - CACert: coretesting.CACert, |
1466 | + Tag: "", |
1467 | + Password: "", |
1468 | + EnvironTag: env.Tag(), |
1469 | + Addrs: []string{srv.Addr()}, |
1470 | + CACert: coretesting.CACert, |
1471 | } |
1472 | return info, func() { |
1473 | err := srv.Stop() |
1474 | @@ -182,7 +185,7 @@ |
1475 | // Now that we are logged in, we see the entity's tag |
1476 | // [0-9.umns] is to handle timestamps that are ns, us, ms, or s |
1477 | // long, though we expect it to be in the 'ms' range. |
1478 | - `-> \[[0-9A-F]+\] machine-0 [0-9.]+[umn]?s {"RequestId":1,"Response":{"Servers":\[\]}} Admin\[""\].Login`, |
1479 | + `-> \[[0-9A-F]+\] machine-0 [0-9.]+[umn]?s {"RequestId":1,"Response":.*} Admin\[""\].Login`, |
1480 | `<- \[[0-9A-F]+\] machine-0 {"RequestId":2,"Type":"Machiner","Request":"Life","Params":{"Entities":\[{"Tag":"machine-0"}\]}}`, |
1481 | `-> \[[0-9A-F]+\] machine-0 [0-9.umns]+ {"RequestId":2,"Response":{"Results":\[{"Life":"alive","Error":null}\]}} Machiner\[""\]\.Life`, |
1482 | }) |
1483 | @@ -461,3 +464,25 @@ |
1484 | c.Check(err, gc.IsNil) |
1485 | } |
1486 | } |
1487 | + |
1488 | +func (s *loginSuite) TestLoginReportsEnvironTag(c *gc.C) { |
1489 | + info, cleanup := s.setupServer(c) |
1490 | + defer cleanup() |
1491 | + // If we call api.Open without giving a username and password, then it |
1492 | + // won't call Login, so we can call it ourselves. |
1493 | + // We Login without passing an EnvironTag, to show that it still lets |
1494 | + // us in, and that we can find out the real EnvironTag from the |
1495 | + // response. |
1496 | + st, err := api.Open(info, fastDialOpts) |
1497 | + c.Assert(err, gc.IsNil) |
1498 | + var result params.LoginResult |
1499 | + creds := ¶ms.Creds{ |
1500 | + AuthTag: "user-admin", |
1501 | + Password: "dummy-secret", |
1502 | + } |
1503 | + err = st.Call("Admin", "", "Login", creds, &result) |
1504 | + c.Assert(err, gc.IsNil) |
1505 | + env, err := s.State.Environment() |
1506 | + c.Assert(err, gc.IsNil) |
1507 | + c.Assert(result.EnvironTag, gc.Equals, env.Tag()) |
1508 | +} |
1509 | |
1510 | === modified file 'state/apiserver/root_test.go' |
1511 | --- state/apiserver/root_test.go 2014-05-20 08:02:01 +0000 |
1512 | +++ state/apiserver/root_test.go 2014-06-05 04:28:36 +0000 |
1513 | @@ -4,8 +4,11 @@ |
1514 | package apiserver_test |
1515 | |
1516 | import ( |
1517 | + "fmt" |
1518 | + "reflect" |
1519 | "time" |
1520 | |
1521 | + jc "github.com/juju/testing/checkers" |
1522 | gc "launchpad.net/gocheck" |
1523 | |
1524 | "launchpad.net/juju-core/rpc/rpcreflect" |
1525 | @@ -82,3 +85,28 @@ |
1526 | case <-time.After(testing.ShortWait): |
1527 | } |
1528 | } |
1529 | + |
1530 | +type errRootSuite struct { |
1531 | + testing.BaseSuite |
1532 | +} |
1533 | + |
1534 | +var _ = gc.Suite(&errRootSuite{}) |
1535 | + |
1536 | +func (s *errRootSuite) TestErrorRoot(c *gc.C) { |
1537 | + origErr := fmt.Errorf("my custom error") |
1538 | + errRoot := apiserver.NewErrRoot(origErr) |
1539 | + st, err := errRoot.Admin("") |
1540 | + c.Check(st, gc.IsNil) |
1541 | + c.Check(err, gc.Equals, origErr) |
1542 | +} |
1543 | + |
1544 | +func (s *errRootSuite) TestErrorRootViaRPC(c *gc.C) { |
1545 | + origErr := fmt.Errorf("my custom error") |
1546 | + errRoot := apiserver.NewErrRoot(origErr) |
1547 | + val := rpcreflect.ValueOf(reflect.ValueOf(errRoot)) |
1548 | + caller, err := val.MethodCaller("Admin", "Login") |
1549 | + c.Assert(err, gc.IsNil) |
1550 | + resp, err := caller.Call("", reflect.Value{}) |
1551 | + c.Check(err, gc.Equals, origErr) |
1552 | + c.Check(resp.IsValid(), jc.IsFalse) |
1553 | +} |
1554 | |
1555 | === modified file 'state/apiserver/server_test.go' |
1556 | --- state/apiserver/server_test.go 2014-04-11 17:51:58 +0000 |
1557 | +++ state/apiserver/server_test.go 2014-06-05 04:28:36 +0000 |
1558 | @@ -4,13 +4,19 @@ |
1559 | package apiserver_test |
1560 | |
1561 | import ( |
1562 | + "crypto/tls" |
1563 | + "crypto/x509" |
1564 | + "fmt" |
1565 | "io" |
1566 | + "net" |
1567 | stdtesting "testing" |
1568 | "time" |
1569 | |
1570 | + "code.google.com/p/go.net/websocket" |
1571 | jc "github.com/juju/testing/checkers" |
1572 | gc "launchpad.net/gocheck" |
1573 | |
1574 | + "launchpad.net/juju-core/cert" |
1575 | jujutesting "launchpad.net/juju-core/juju/testing" |
1576 | "launchpad.net/juju-core/rpc" |
1577 | "launchpad.net/juju-core/state" |
1578 | @@ -208,3 +214,48 @@ |
1579 | c.Assert(err, gc.IsNil) |
1580 | c.Assert(alive, gc.Equals, isAlive) |
1581 | } |
1582 | + |
1583 | +func dialWebsocket(c *gc.C, addr, path string) (*websocket.Conn, error) { |
1584 | + origin := "http://localhost/" |
1585 | + url := fmt.Sprintf("wss://%s%s", addr, path) |
1586 | + config, err := websocket.NewConfig(url, origin) |
1587 | + c.Assert(err, gc.IsNil) |
1588 | + pool := x509.NewCertPool() |
1589 | + xcert, err := cert.ParseCert(coretesting.CACert) |
1590 | + c.Assert(err, gc.IsNil) |
1591 | + pool.AddCert(xcert) |
1592 | + config.TlsConfig = &tls.Config{RootCAs: pool} |
1593 | + return websocket.DialConfig(config) |
1594 | +} |
1595 | + |
1596 | +func (s *serverSuite) TestNonCompatiblePathsAre404(c *gc.C) { |
1597 | + // we expose the API at '/' for compatibility, and at '/ENVUUID/api' |
1598 | + // for the correct location, but other Paths should fail. |
1599 | + srv, err := apiserver.NewServer( |
1600 | + s.State, "localhost:0", |
1601 | + []byte(coretesting.ServerCert), []byte(coretesting.ServerKey), |
1602 | + "", "") |
1603 | + c.Assert(err, gc.IsNil) |
1604 | + defer srv.Stop() |
1605 | + // We have to use 'localhost' because that is what the TLS cert says. |
1606 | + // So find just the Port for the server |
1607 | + _, portString, err := net.SplitHostPort(srv.Addr()) |
1608 | + c.Assert(err, gc.IsNil) |
1609 | + addr := "localhost:" + portString |
1610 | + // '/' should be fine |
1611 | + conn, err := dialWebsocket(c, addr, "/") |
1612 | + c.Assert(err, gc.IsNil) |
1613 | + conn.Close() |
1614 | + // '/ENVIRONUUID/api' should be fine |
1615 | + conn, err = dialWebsocket(c, addr, "/environ-uuid/api") |
1616 | + c.Assert(err, gc.IsNil) |
1617 | + conn.Close() |
1618 | + |
1619 | + // '/randompath' is not ok |
1620 | + conn, err = dialWebsocket(c, addr, "/randompath") |
1621 | + // Unfortunately go.net/websocket just returns Bad Status, it doesn't |
1622 | + // give us any information (whether this was a 404 Not Found, Internal |
1623 | + // Server Error, 200 OK, etc.) |
1624 | + c.Assert(err, gc.ErrorMatches, `websocket.Dial wss://localhost:\d+/randompath: bad status`) |
1625 | + c.Assert(conn, gc.IsNil) |
1626 | +} |
1627 | |
1628 | === modified file 'state/apiserver/tools.go' |
1629 | --- state/apiserver/tools.go 2014-04-07 04:50:31 +0000 |
1630 | +++ state/apiserver/tools.go 2014-06-05 04:28:36 +0000 |
1631 | @@ -34,6 +34,10 @@ |
1632 | h.authError(w, h) |
1633 | return |
1634 | } |
1635 | + if err := h.validateEnvironUUID(r); err != nil { |
1636 | + h.sendError(w, http.StatusNotFound, err.Error()) |
1637 | + return |
1638 | + } |
1639 | |
1640 | switch r.Method { |
1641 | case "POST": |
1642 | |
1643 | === modified file 'state/apiserver/tools_test.go' |
1644 | --- state/apiserver/tools_test.go 2014-05-23 11:24:54 +0000 |
1645 | +++ state/apiserver/tools_test.go 2014-06-05 04:28:36 +0000 |
1646 | @@ -5,10 +5,11 @@ |
1647 | |
1648 | import ( |
1649 | "encoding/json" |
1650 | + "fmt" |
1651 | "io/ioutil" |
1652 | "net/http" |
1653 | + "net/url" |
1654 | "path" |
1655 | - "path/filepath" |
1656 | |
1657 | gc "launchpad.net/gocheck" |
1658 | |
1659 | @@ -101,17 +102,21 @@ |
1660 | c, resp, http.StatusBadRequest, "expected Content-Type: application/x-tar-gz, got: application/octet-stream") |
1661 | } |
1662 | |
1663 | +func (s *toolsSuite) setupToolsForUpload(c *gc.C) (coretools.List, version.Binary, string) { |
1664 | + localStorage := c.MkDir() |
1665 | + vers := version.MustParseBinary("1.9.0-quantal-amd64") |
1666 | + versionStrings := []string{vers.String()} |
1667 | + expectedTools := toolstesting.MakeToolsWithCheckSum(c, localStorage, "releases", versionStrings) |
1668 | + toolsFile := tools.StorageName(vers) |
1669 | + return expectedTools, vers, path.Join(localStorage, toolsFile) |
1670 | +} |
1671 | + |
1672 | func (s *toolsSuite) TestUpload(c *gc.C) { |
1673 | // Make some fake tools. |
1674 | - localStorage := c.MkDir() |
1675 | - vers := version.MustParseBinary("1.9.0-quantal-amd64") |
1676 | - versionStrings := []string{vers.String()} |
1677 | - expectedTools := toolstesting.MakeToolsWithCheckSum(c, localStorage, "releases", versionStrings) |
1678 | - |
1679 | + expectedTools, vers, toolPath := s.setupToolsForUpload(c) |
1680 | // Now try uploading them. |
1681 | - toolsFile := tools.StorageName(vers) |
1682 | resp, err := s.uploadRequest( |
1683 | - c, s.toolsURI(c, "?binaryVersion="+vers.String()), true, path.Join(localStorage, toolsFile)) |
1684 | + c, s.toolsURI(c, "?binaryVersion="+vers.String()), true, toolPath) |
1685 | c.Assert(err, gc.IsNil) |
1686 | |
1687 | // Check the response. |
1688 | @@ -126,22 +131,59 @@ |
1689 | c.Assert(err, gc.IsNil) |
1690 | uploadedData, err := ioutil.ReadAll(r) |
1691 | c.Assert(err, gc.IsNil) |
1692 | - expectedData, err := ioutil.ReadFile(filepath.Join(localStorage, tools.StorageName(vers))) |
1693 | + expectedData, err := ioutil.ReadFile(toolPath) |
1694 | c.Assert(err, gc.IsNil) |
1695 | c.Assert(uploadedData, gc.DeepEquals, expectedData) |
1696 | } |
1697 | |
1698 | +func (s *toolsSuite) TestUploadAllowsTopLevelPath(c *gc.C) { |
1699 | + // Backwards compatibility check, that we can upload tools to |
1700 | + // https://host:port/tools |
1701 | + expectedTools, vers, toolPath := s.setupToolsForUpload(c) |
1702 | + url := s.toolsURL(c, "binaryVersion="+vers.String()) |
1703 | + url.Path = "/tools" |
1704 | + resp, err := s.uploadRequest(c, url.String(), true, toolPath) |
1705 | + c.Assert(err, gc.IsNil) |
1706 | + // Check the response. |
1707 | + stor := s.Conn.Environ.Storage() |
1708 | + toolsURL, err := stor.URL(tools.StorageName(vers)) |
1709 | + c.Assert(err, gc.IsNil) |
1710 | + expectedTools[0].URL = toolsURL |
1711 | + s.assertUploadResponse(c, resp, expectedTools[0]) |
1712 | +} |
1713 | + |
1714 | +func (s *toolsSuite) TestUploadAllowsEnvUUIDPath(c *gc.C) { |
1715 | + // Check that we can upload tools to https://host:port/ENVUUID/tools |
1716 | + environ, err := s.State.Environment() |
1717 | + c.Assert(err, gc.IsNil) |
1718 | + expectedTools, vers, toolPath := s.setupToolsForUpload(c) |
1719 | + url := s.toolsURL(c, "binaryVersion="+vers.String()) |
1720 | + url.Path = fmt.Sprintf("/%s/tools", environ.UUID()) |
1721 | + resp, err := s.uploadRequest(c, url.String(), true, toolPath) |
1722 | + c.Assert(err, gc.IsNil) |
1723 | + // Check the response. |
1724 | + stor := s.Conn.Environ.Storage() |
1725 | + toolsURL, err := stor.URL(tools.StorageName(vers)) |
1726 | + c.Assert(err, gc.IsNil) |
1727 | + expectedTools[0].URL = toolsURL |
1728 | + s.assertUploadResponse(c, resp, expectedTools[0]) |
1729 | +} |
1730 | + |
1731 | +func (s *toolsSuite) TestUploadRejectsWrongEnvUUIDPath(c *gc.C) { |
1732 | + // Check that we cannot access the tools at https://host:port/BADENVUUID/tools |
1733 | + url := s.toolsURL(c, "") |
1734 | + url.Path = "/dead-beef-123456/tools" |
1735 | + resp, err := s.authRequest(c, "POST", url.String(), "", nil) |
1736 | + c.Assert(err, gc.IsNil) |
1737 | + s.assertErrorResponse(c, resp, http.StatusNotFound, `unknown environment: "dead-beef-123456"`) |
1738 | +} |
1739 | + |
1740 | func (s *toolsSuite) TestUploadFakeSeries(c *gc.C) { |
1741 | // Make some fake tools. |
1742 | - localStorage := c.MkDir() |
1743 | - vers := version.MustParseBinary("1.9.0-quantal-amd64") |
1744 | - versionStrings := []string{vers.String()} |
1745 | - expectedTools := toolstesting.MakeToolsWithCheckSum(c, localStorage, "releases", versionStrings) |
1746 | - |
1747 | + expectedTools, vers, toolPath := s.setupToolsForUpload(c) |
1748 | // Now try uploading them. |
1749 | - toolsFile := tools.StorageName(vers) |
1750 | params := "?binaryVersion=" + vers.String() + "&series=precise,trusty" |
1751 | - resp, err := s.uploadRequest(c, s.toolsURI(c, params), true, path.Join(localStorage, toolsFile)) |
1752 | + resp, err := s.uploadRequest(c, s.toolsURI(c, params), true, toolPath) |
1753 | c.Assert(err, gc.IsNil) |
1754 | |
1755 | // Check the response. |
1756 | @@ -159,16 +201,24 @@ |
1757 | c.Assert(err, gc.IsNil) |
1758 | uploadedData, err := ioutil.ReadAll(r) |
1759 | c.Assert(err, gc.IsNil) |
1760 | - expectedData, err := ioutil.ReadFile(filepath.Join(localStorage, tools.StorageName(vers))) |
1761 | + expectedData, err := ioutil.ReadFile(toolPath) |
1762 | c.Assert(err, gc.IsNil) |
1763 | c.Assert(uploadedData, gc.DeepEquals, expectedData) |
1764 | } |
1765 | } |
1766 | |
1767 | +func (s *toolsSuite) toolsURL(c *gc.C, query string) *url.URL { |
1768 | + uri := s.baseURL(c) |
1769 | + uri.Path += "/tools" |
1770 | + uri.RawQuery = query |
1771 | + return uri |
1772 | +} |
1773 | + |
1774 | func (s *toolsSuite) toolsURI(c *gc.C, query string) string { |
1775 | - _, info, err := s.APIConn.Environ.StateInfo() |
1776 | - c.Assert(err, gc.IsNil) |
1777 | - return "https://" + info.Addrs[0] + "/tools" + query |
1778 | + if query != "" && query[0] == '?' { |
1779 | + query = query[1:] |
1780 | + } |
1781 | + return s.toolsURL(c, query).String() |
1782 | } |
1783 | |
1784 | func (s *toolsSuite) assertUploadResponse(c *gc.C, resp *http.Response, agentTools *coretools.Tools) { |
Reviewers: mp+221632_ code.launchpad. net,
Message:
Please take a look.
Description:
state/*: login to /ENVUUID/api URLs
This is an evolution of: /codereview. appspot. com/101760046/
https:/
https:/ /code.launchpad .net/~jameinel/ juju-core/ login-returns- env-tag/ +merge/ 221021
Instead of having Login itself take an Environment Tag, it changes our address: host/" we now will try to connect to address: host/ENVUUID/ api".
API so that we connect to a different URL. Instead of
"wss://
"wss://
The change itself involves a few variables: com/bmizerany/ pat", which is a reasonably
1) A new dependency "github.
simple library that lets you put in patterns to your http.Mux, which
will then transform "/:path/foo" as matching anything in ":path" and
adding that as a Query argument instead.
2) Using that library to expose /:environ/api and .../log, .../charms,
.../tools. This patch itself doesn't make use of the new URLs, because
it is not backwards compatible, and I didn't want to quite sort out how
to do the backwards compatibility yet.
3) We do use the environ/api one when we know the Environment UUID,
which Login will return for us (and we will cache).
I did test that this still works against a 1.18 server (it connects to
the /ENVUUID/api address, but as that is a child of / it still works). I
also ran into something a bit surprising, but probably ok. If you
actually force the environ-uuid to be invalid in your .jenv file, the
code will try to connect, and fail with InvalidEnviron. However, our
connection code has lots of resilience built in, so we end up just
falling back to the Config fallback open, which naturally logs in as
just environUUID="", which then finds the right environment UUID and
caches it.
So this code doesn't fix all possible paths, but I think it improves the
state of the world that I'd like to land it and get back to API
versioning.
https:/ /code.launchpad .net/~jameinel/ juju-core/ login-env- urls/+merge/ 221632
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/102920048/
Affected files (+789, -94 lines): configstore/ disk.go configstore/ interface. go configstore/ interface_ test.go test.go apiclient. go apiclient_ test.go client_ test.go export_ test.go params/ params. go state_test. go /admin. go /apiserver. go /charms. go /charms_ test.go /common/ errors. go /debuglog. go /debuglog_ test.go /export_ test.go /httphandler. go /login_ test.go /root_test. go /tools. go /tools_ test.go
A [revision details]
M dependencies.tsv
M environs/
M environs/
M environs/
M juju/api.go
M juju/apiconn_
M juju/mock_test.go
M state/api/
M state/api/
M state/api/
M state/api/
M state/api/
M state/api/state.go
M state/api/
M state/apiserver
M state/apiserver
M state/apiserver
M state/apiserver
M state/apiserver
M state/apiserver
M state/apiserver
M state/apiserver
M state/apiserver
M state/apiserver
M state/apiserver
M state/apiserver
M state/apiserver