Merge lp:~jameinel/juju-core/login-env-urls into lp:~go-bot/juju-core/trunk

Proposed by John A Meinel
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
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+221632@code.launchpad.net

Description of the change

state/*: login to /ENVUUID/api URLs

This is an evolution of:
  https://codereview.appspot.com/101760046/
  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
API so that we connect to a different URL. Instead of
"wss://address:host/" we now will try to connect to
"wss://address:host/ENVUUID/api".

The change itself involves a few variables:
 1) A new dependency "github.com/bmizerany/pat", which is a reasonably
 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://codereview.appspot.com/102920048/

To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) wrote :

Reviewers: mp+221632_code.launchpad.net,

Message:
Please take a look.

Description:
state/*: login to /ENVUUID/api URLs

This is an evolution of:
   https://codereview.appspot.com/101760046/

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
API so that we connect to a different URL. Instead of
"wss://address:host/" we now will try to connect to
"wss://address:host/ENVUUID/api".

The change itself involves a few variables:
  1) A new dependency "github.com/bmizerany/pat", which is a reasonably
  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):
   A [revision details]
   M dependencies.tsv
   M environs/configstore/disk.go
   M environs/configstore/interface.go
   M environs/configstore/interface_test.go
   M juju/api.go
   M juju/apiconn_test.go
   M juju/mock_test.go
   M state/api/apiclient.go
   M state/api/apiclient_test.go
   M state/api/client_test.go
   M state/api/export_test.go
   M state/api/params/params.go
   M state/api/state.go
   M state/api/state_test.go
   M state/apiserver/admin.go
   M state/apiserver/apiserver.go
   M state/apiserver/charms.go
   M state/apiserver/charms_test.go
   M state/apiserver/common/errors.go
   M state/apiserver/debuglog.go
   M state/apiserver/debuglog_test.go
   M state/apiserver/export_test.go
   M state/apiserver/httphandler.go
   M state/apiserver/login_test.go
   M state/apiserver/root_test.go
   M state/apiserver/tools.go
   M state/apiserver/tools_test.go

2818. By John A Meinel

review changes from AXW's review.

2819. By John A Meinel

merge trunk, resolve conflicts

Revision history for this message
John A Meinel (jameinel) wrote :
Revision history for this message
Roger Peppe (rogpeppe) wrote :
Download full text (3.6 KiB)

Looks great in general, with a few queries and suggestions.

https://codereview.appspot.com/102920048/diff/20001/environs/configstore/interface.go
File environs/configstore/interface.go (right):

https://codereview.appspot.com/102920048/diff/20001/environs/configstore/interface.go#newcode25
environs/configstore/interface.go:25: EnvironUUID string
Why is this not an environ tag, to match the tag in api.Info ?

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

https://codereview.appspot.com/102920048/diff/20001/state/api/apiclient.go#newcode151
state/api/apiclient.go:151: environUUID = envUUID
why not just use environ tag throughout?

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

https://codereview.appspot.com/102920048/diff/20001/state/api/export_test.go#newcode10
state/api/export_test.go:10: SetupWebsocket = setupWebsocket
s/Setup/SetUp/
?

https://codereview.appspot.com/102920048/diff/20001/state/apiserver/apiserver.go
File state/apiserver/apiserver.go (right):

https://codereview.appspot.com/102920048/diff/20001/state/apiserver/apiserver.go#newcode184
state/apiserver/apiserver.go:184: handleAll(mux, "/:envuuid/log",
One passing thought - we might be slightly more future proof and
"obviously right" if the path was "/environ/$UUID/..." as we wouldn't
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://codereview.appspot.com/102920048/diff/20001/state/apiserver/apiserver.go#newcode195
state/apiserver/apiserver.go:195: // pat only does "text/plain"
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://codereview.appspot.com/102920048/diff/20001/state/apiserver/apiserver.go#newcode210
state/apiserver/apiserver.go:210: handleAll(mux, "/",
http.HandlerFunc(srv.apiHandler))
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://codereview.appspot.com/102920048/diff/20001/state/apiserver/apiserver.go#newcode249
state/apiserver/apiserver.go:249: // 2) On firt connect. The environment
UUID is currently
s/firt/first/

https://codereview.appspot.com/102920048/diff/20001/state/apiserver/apiserver.go#newcode256
state/apiserver/apiserver.go:256: env, err := srv.state.Environment()
Rather than add an extra mongo round trip to every http request, we
could store the UUID in the server.

https://codereview.appspot.com/102920048/diff/20001/state/apiserver/common/errors.go
File state/apiserver/common/errors.go (right):

https://codereview.appspot.com/102920048/diff/20001/state/apiserver/common/errors.go#newcode50
state/apiserver/common/errors.go:50: ErrInvalidEnviron =
stder...

Read more...

Revision history for this message
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.

https://codereview.appspot.com/102920048/

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

Revision history for this message
John A Meinel (jameinel) wrote :
Download full text (3.8 KiB)

I had written this up, but it didn't get published because I switched to
git.

https://codereview.appspot.com/102920048/diff/20001/environs/configstore/interface.go
File environs/configstore/interface.go (right):

https://codereview.appspot.com/102920048/diff/20001/environs/configstore/interface.go#newcode25
environs/configstore/interface.go:25: EnvironUUID string
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://codereview.appspot.com/102920048/diff/20001/state/api/apiclient.go
File state/api/apiclient.go (right):

https://codereview.appspot.com/102920048/diff/20001/state/api/apiclient.go#newcode151
state/api/apiclient.go:151: environUUID = envUUID
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://codereview.appspot.com/102920048/diff/20001/state/api/export_test.go
File state/api/export_test.go (right):

https://codereview.appspot.com/102920048/diff/20001/state/api/export_test.go#newcode10
state/api/export_test.go:10: SetupWebsocket = setupWebsocket
On 2014/06/02 10:08:17, rog wrote:
> s/Setup/SetUp/
> ?

Done.

https://codereview.appspot.com/102920048/diff/20001/state/apiserver/apiserver.go
File state/apiserver/apiserver.go (right):

https://codereview.appspot.com/102920048/diff/20001/state/apiserver/apiserver.go#newcode210
state/apiserver/apiserver.go:210: handleAll(mux, "/",
http.HandlerFunc(srv.apiHandler))
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://codereview.appspot.com/102920048/diff/20001/state/apiserver/apiserver.go#newcode256
state/apiserver/apiserver.go:256: env, err := srv.state.Environment()
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://codereview.appspot.com/102920048/diff/20001/state/apiserver/common/errors.go
File state/apiserver/common/errors.go (right):

https://codereview.appspot.com/102920048/diff/20001/state/apiserver/common/errors.go#newcode50
state/apiserver/common/errors.go:50: ErrInvalidEnviron =
stderrors.New("invalid environment requested")
On 2014/06/02 10:08:18, rog wrote:
> ErrUnknownEnv...

Read more...

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

https://codereview.appspot.com/102920048/diff/20001/state/apiserver/apiserver.go
File state/apiserver/apiserver.go (right):

https://codereview.appspot.com/102920048/diff/20001/state/apiserver/apiserver.go#newcode210
state/apiserver/apiserver.go:210: handleAll(mux, "/",
http.HandlerFunc(srv.apiHandler))
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

https://codereview.appspot.com/102920048/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 := &params.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) {

Subscribers

People subscribed via source and target branches

to status/vote changes: