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

Proposed by John A Meinel
Status: Work in progress
Proposed branch: lp:~jameinel/juju-core/login-returns-env-tag
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 851 lines (+402/-41)
14 files modified
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 (+14/-1)
state/api/apiclient_test.go (+22/-0)
state/api/params/params.go (+6/-4)
state/api/state.go (+6/-4)
state/api/state_test.go (+48/-0)
state/apiserver/admin.go (+21/-1)
state/apiserver/common/errors.go (+2/-0)
state/apiserver/login_test.go (+82/-8)
To merge this branch: bzr merge lp:~jameinel/juju-core/login-returns-env-tag
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+221021@code.launchpad.net

Description of the change

various: change API.Login() to handle EnvironTag

This changes a bit of Login handling to allow us to pass in an
EnvironTag which will get validated against the running environment. We
also return EnvironTag as part of the Login response, since otherwise
the existing client doesn't have a good way of finding out that
information.

It then goes around and updates a bunch of places so that we can store
the EnvironTag in our .jenv and read it back out (and pass it in).

I went with a new error message (invalid environment requested), rather
than just ErrPerm, partially because I have already validated the Login
credentials. (Since one user is intended to have access to multiple
environments, it made sense to layer it that way). It does still return
the CodeUnauthorized.

It is still valid to pass in an EnvironTag of "". Eventually we may
change how Login works under that circumstance (for Multi Environment
support), but for now this gets us closer to where we want to be.

If we get to the point that Bootstrap actually stashes our api
information it could also stash the environment tag. However, in the
short term if it just connects to the API like everything else, it will
get the environment UUID cached, which will let us be a bit more
protected from accidentally connecting to the wrong environment.

This might have implications for CI, as I believe they try to re-use
things like CACerts so that they can use the same .jenv files across
mulitple bootstraps. If so, this will start causing the client to get
rejected because the actual environment has changed. Though I think that
is the behavior that we decided we wanted.

https://codereview.appspot.com/101760046/

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

Reviewers: mp+221021_code.launchpad.net,

Message:
Please take a look.

Description:
various: change API.Login() to handle EnvironTag

This changes a bit of Login handling to allow us to pass in an
EnvironTag which will get validated against the running environment. We
also return EnvironTag as part of the Login response, since otherwise
the existing client doesn't have a good way of finding out that
information.

It then goes around and updates a bunch of places so that we can store
the EnvironTag in our .jenv and read it back out (and pass it in).

I went with a new error message (invalid environment requested), rather
than just ErrPerm, partially because I have already validated the Login
credentials. (Since one user is intended to have access to multiple
environments, it made sense to layer it that way). It does still return
the CodeUnauthorized.

It is still valid to pass in an EnvironTag of "". Eventually we may
change how Login works under that circumstance (for Multi Environment
support), but for now this gets us closer to where we want to be.

If we get to the point that Bootstrap actually stashes our api
information it could also stash the environment tag. However, in the
short term if it just connects to the API like everything else, it will
get the environment UUID cached, which will let us be a bit more
protected from accidentally connecting to the wrong environment.

This might have implications for CI, as I believe they try to re-use
things like CACerts so that they can use the same .jenv files across
mulitple bootstraps. If so, this will start causing the client to get
rejected because the actual environment has changed. Though I think that
is the behavior that we decided we wanted.

https://code.launchpad.net/~jameinel/juju-core/login-returns-env-tag/+merge/221021

(do not edit description out of merge proposal)

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

Affected files (+387, -40 lines):
   A [revision details]
   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/params/params.go
   M state/api/state.go
   M state/api/state_test.go
   M state/apiserver/admin.go
   M state/apiserver/common/errors.go
   M state/apiserver/login_test.go

Revision history for this message
William Reade (fwereade) wrote :

This looks good. In the absence of completed API versioning I'd actually
be happy to land this as is -- but I'd appreciate your thoughts on
consolidating the configstore and agentconfig bits...

https://codereview.appspot.com/101760046/diff/1/environs/configstore/disk.go
File environs/configstore/disk.go (right):

https://codereview.appspot.com/101760046/diff/1/environs/configstore/disk.go#newcode144
environs/configstore/disk.go:144: EnvironTag: info.EnvInfo.EnvironTag,
I think we want to *store* the uuid, not the tag -- the tags really are
just for the API, and I would prefer that we not let them leak into
users' worldviews. Tags coming back from the API are fine and good, but
no further.

https://codereview.appspot.com/101760046/diff/1/environs/configstore/interface_test.go
File environs/configstore/interface_test.go (right):

https://codereview.appspot.com/101760046/diff/1/environs/configstore/interface_test.go#newcode51
environs/configstore/interface_test.go:51: EnvironTag:
"environ-dead-beef",
this choice of "uuid" makes me unreasonably happy :)

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

https://codereview.appspot.com/101760046/diff/1/juju/api.go#newcode358
juju/api.go:358: EnvironTag: apiInfo.EnvironTag,
I won't keep saying it, but this is the place to just extract the uuid
and store it.

https://codereview.appspot.com/101760046/diff/1/juju/api.go#newcode387
juju/api.go:387: _ = newEnvironTag
?

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

https://codereview.appspot.com/101760046/diff/1/state/apiserver/admin.go#newcode142
state/apiserver/admin.go:142: // trigger an error for the Environment()
call?
Heh. You could, inside state, slip in and delete the document; but
Idon't think there's a way to do it here. (Stop the mongo? ;p)

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

https://codereview.appspot.com/101760046/diff/1/state/apiserver/login_test.go#newcode530
state/apiserver/login_test.go:530: }
I guess we have enough tests that it accepts a missing environ tag
already?

I'm wondering whether we should be fixing this stuff for the agents as
well. It's less pressing, but still.

(FWIW, I feel that all this code to connect and store changes etc etc
really should be exactly the same on agent and client -- the only
interesting difference is the extra stuff that agents need to do to
change their passwords, and the client will need the same functionality
once we expose pass-word-changing there.)

https://codereview.appspot.com/101760046/

2801. By John A Meinel

clean up the code so that we only store EnvironUUID and just talk about EnvironTag in the api calls

2802. By John A Meinel

clarify in the comments that we are explicitly testing the case where EnvironTag is empty.

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

Please take a look.

https://codereview.appspot.com/101760046/diff/1/environs/configstore/disk.go
File environs/configstore/disk.go (right):

https://codereview.appspot.com/101760046/diff/1/environs/configstore/disk.go#newcode144
environs/configstore/disk.go:144: EnvironTag: info.EnvInfo.EnvironTag,
On 2014/05/27 07:27:57, fwereade wrote:
> I think we want to *store* the uuid, not the tag -- the tags really
are just for
> the API, and I would prefer that we not let them leak into users'
worldviews.
> Tags coming back from the API are fine and good, but no further.

Fine with me. I wasn't sure if bare UUIDs were better to store than
tags. I realized that in the API things are tags, and in State they are
IDs, but wasn't sure about user level stuff.

Done.

I ended up sticking with the api.State object has an EnvironTag()
method, and api.Info thinks of tags, but configstore.EnvironInfo uses
EnvironUUID.

https://codereview.appspot.com/101760046/diff/1/environs/configstore/interface_test.go
File environs/configstore/interface_test.go (right):

https://codereview.appspot.com/101760046/diff/1/environs/configstore/interface_test.go#newcode51
environs/configstore/interface_test.go:51: EnvironTag:
"environ-dead-beef",
On 2014/05/27 07:27:57, fwereade wrote:
> this choice of "uuid" makes me unreasonably happy :)

:)

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

https://codereview.appspot.com/101760046/diff/1/juju/api.go#newcode358
juju/api.go:358: EnvironTag: apiInfo.EnvironTag,
On 2014/05/27 07:27:57, fwereade wrote:
> I won't keep saying it, but this is the place to just extract the uuid
and store
> it.

Done.

https://codereview.appspot.com/101760046/diff/1/juju/api.go#newcode358
juju/api.go:358: EnvironTag: apiInfo.EnvironTag,
On 2014/05/27 07:27:57, fwereade wrote:
> I won't keep saying it, but this is the place to just extract the uuid
and store
> it.

Done.

https://codereview.appspot.com/101760046/diff/1/juju/api.go#newcode387
juju/api.go:387: _ = newEnvironTag
On 2014/05/27 07:27:57, fwereade wrote:
> ?

temporary workaround for golang not allowing unused vars while
incrementally building code. removed.

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

https://codereview.appspot.com/101760046/diff/1/state/apiserver/admin.go#newcode142
state/apiserver/admin.go:142: // trigger an error for the Environment()
call?
On 2014/05/27 07:27:58, fwereade wrote:
> Heh. You could, inside state, slip in and delete the document; but
Idon't think
> there's a way to do it here. (Stop the mongo? ;p)

Yeah, it is just one of those things where you "have" to propagate
errors, but I really don't like touching the stuff without actually
checking what it does.
Should this actually be returning some other error for the API? Is it
sane to just blat out whatever internal failure occured?

We don't really have concrete decisions around this sort of thing (that
I can tell).

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

https://codereview.appspot.com/101760046/diff/1/state/apiserver/login_t...

Read more...

Revision history for this message
John A Meinel (jameinel) wrote :
Revision history for this message
Andrew Wilkins (axwalk) wrote :

LGTM

https://codereview.appspot.com/101760046/diff/40001/juju/api.go
File juju/api.go (right):

https://codereview.appspot.com/101760046/diff/40001/juju/api.go#newcode271
juju/api.go:271: environUUID := ""
s/environUUID/environTag/

https://codereview.appspot.com/101760046/diff/40001/juju/apiconn_test.go
File juju/apiconn_test.go (right):

https://codereview.appspot.com/101760046/diff/40001/juju/apiconn_test.go#newcode283
juju/apiconn_test.go:283: // The local cache doesn't have an EnvironTag,
which the API does
I think this is testing something that is currently impossible. I
suppose it doesn't hurt to be cautious of bugs introduced in the server
though.

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

https://codereview.appspot.com/101760046/diff/40001/state/api/state_test.go#newcode105
state/api/state_test.go:105: c.Check(apistate.EnvironTag(), gc.Equals,
env.Tag())
An explicit test for Login with envtag=="" would be good here.

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

https://codereview.appspot.com/101760046/diff/40001/state/apiserver/admin.go#newcode138
state/apiserver/admin.go:138: if c.EnvironTag != "" {
Perhaps add a comment here, saying why we allow an unspecified
EnvironTag, lest someone thinks it's a bug and breaks it?

https://codereview.appspot.com/101760046/diff/40001/state/apiserver/login_test.go
File state/apiserver/login_test.go (right):

https://codereview.appspot.com/101760046/diff/40001/state/apiserver/login_test.go#newcode519
state/apiserver/login_test.go:519:
func (s *loginSuite) TestLoginAcceptsEmptyEnvironTag(c *gc.C) {
...
}

?

https://codereview.appspot.com/101760046/

Revision history for this message
John A Meinel (jameinel) wrote :

Thanks for your review. I've superseded this request with this one:
https://codereview.appspot.com/102920048/

However, hopefully I've managed to address/respond to all of the
comments you've made here.

https://codereview.appspot.com/101760046/diff/40001/juju/api.go
File juju/api.go (right):

https://codereview.appspot.com/101760046/diff/40001/juju/api.go#newcode271
juju/api.go:271: environUUID := ""
On 2014/05/29 03:59:25, axw wrote:
> s/environUUID/environTag/

Done.

https://codereview.appspot.com/101760046/diff/40001/juju/apiconn_test.go
File juju/apiconn_test.go (right):

https://codereview.appspot.com/101760046/diff/40001/juju/apiconn_test.go#newcode283
juju/apiconn_test.go:283: // The local cache doesn't have an EnvironTag,
which the API does
On 2014/05/29 03:59:25, axw wrote:
> I think this is testing something that is currently impossible. I
suppose it
> doesn't hurt to be cautious of bugs introduced in the server though.

It actually happens in the test suite. Specifically our test suite has a
lot of places where we have no machine-0 (and thus no API host ports).

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

https://codereview.appspot.com/101760046/diff/40001/state/api/state_test.go#newcode105
state/api/state_test.go:105: c.Check(apistate.EnvironTag(), gc.Equals,
env.Tag())
On 2014/05/29 03:59:25, axw wrote:
> An explicit test for Login with envtag=="" would be good here.

I believe I have that test in the follow up.

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

https://codereview.appspot.com/101760046/diff/40001/state/apiserver/admin.go#newcode138
state/apiserver/admin.go:138: if c.EnvironTag != "" {
On 2014/05/29 03:59:25, axw wrote:
> Perhaps add a comment here, saying why we allow an unspecified
EnvironTag, lest
> someone thinks it's a bug and breaks it?

This code is now moved, because the EnvironUUID is in the URL instead of
as a parameter to Login.

However, I added what is hopefully a good comment to
"validateEnvironUUID".

https://codereview.appspot.com/101760046/diff/40001/state/apiserver/login_test.go
File state/apiserver/login_test.go (right):

https://codereview.appspot.com/101760046/diff/40001/state/apiserver/login_test.go#newcode519
state/apiserver/login_test.go:519:
On 2014/05/29 03:59:25, axw wrote:
> func (s *loginSuite) TestLoginAcceptsEmptyEnvironTag(c *gc.C) {
> ...
> }

> ?

If you look, "TestLoginReportsEnvironTag" explicitly passes
EnvironTag:"" which is the test you're looking for. Would you prefer a
different name?

https://codereview.appspot.com/101760046/

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

https://codereview.appspot.com/101760046/diff/40001/state/apiserver/login_test.go
File state/apiserver/login_test.go (right):

https://codereview.appspot.com/101760046/diff/40001/state/apiserver/login_test.go#newcode519
state/apiserver/login_test.go:519:
On 2014/06/01 09:53:31, jameinel wrote:
> On 2014/05/29 03:59:25, axw wrote:
> > func (s *loginSuite) TestLoginAcceptsEmptyEnvironTag(c *gc.C) {
> > ...
> > }
> >
> > ?

> If you look, "TestLoginReportsEnvironTag" explicitly passes
EnvironTag:"" which
> is the test you're looking for. Would you prefer a different name?

Sorry, don't know how I missed that. That's fine.

https://codereview.appspot.com/101760046/

Revision history for this message
John A Meinel (jameinel) wrote :

np, William missed it as well. The naming probably leads reviews to think
about "what about this case" and there isn't an obviously named test doing
it.

On Sun, Jun 1, 2014 at 4:15 PM, Andrew Wilkins <<email address hidden>
> wrote:

>
>
> https://codereview.appspot.com/101760046/diff/40001/state/apiserver/login_test.go
> File state/apiserver/login_test.go (right):
>
>
> https://codereview.appspot.com/101760046/diff/40001/state/apiserver/login_test.go#newcode519
> state/apiserver/login_test.go:519:
> On 2014/06/01 09:53:31, jameinel wrote:
> > On 2014/05/29 03:59:25, axw wrote:
> > > func (s *loginSuite) TestLoginAcceptsEmptyEnvironTag(c *gc.C) {
> > > ...
> > > }
> > >
> > > ?
>
> > If you look, "TestLoginReportsEnvironTag" explicitly passes
> EnvironTag:"" which
> > is the test you're looking for. Would you prefer a different name?
>
>
> Sorry, don't know how I missed that. That's fine.
>
> https://codereview.appspot.com/101760046/
>
> --
>
> https://code.launchpad.net/~jameinel/juju-core/login-returns-env-tag/+merge/221021
> You are the owner of lp:~jameinel/juju-core/login-returns-env-tag.
>

Unmerged revisions

2802. By John A Meinel

clarify in the comments that we are explicitly testing the case where EnvironTag is empty.

2801. By John A Meinel

clean up the code so that we only store EnvironUUID and just talk about EnvironTag in the api calls

2800. By John A Meinel

Fix another edge case.

Our test suite doesn't always have APIHostPorts available, because we don't
always have a machine-0. In those cases, we can get an EnvironTag but not
have any addresses to connect to. We don't want to end up setting the
connection addresses to the empty list, and make it hard to actually
connect to the environment.
Add a test, move where we set addrs to only set it when they have changed.

2799. By John A Meinel

Fixup the APIServer tests now that Login takes the extra argument

2798. By John A Meinel

Test that Open and Login will pass the EnvironTag when requested.

2797. By John A Meinel

update another environs/configstore test for the new tag

2796. By John A Meinel

Thread the EnvironTag code through the testing mocks at least.
The 'juju' test suite is happy.

2795. By John A Meinel

Start working on threading EnvironTag through all the apis, not quite working yet.

2794. By John A Meinel

rename Environ to EnvironTag.

2793. By John A Meinel

Admin.Login accepts environ tags, and will give you a valid error when it doesn't know about the UUID you are requesting.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'environs/configstore/disk.go'
2--- environs/configstore/disk.go 2014-05-13 04:50:10 +0000
3+++ environs/configstore/disk.go 2014-05-27 09:53:17 +0000
4@@ -32,6 +32,7 @@
5 type EnvironInfoData struct {
6 User string
7 Password string
8+ EnvironUUID string `json:"environ-uuid,omitempty" yaml:"environ-uuid,omitempty"`
9 StateServers []string `json:"state-servers" yaml:"state-servers"`
10 CACert string `json:"ca-cert" yaml:"ca-cert"`
11 Config map[string]interface{} `json:"bootstrap-config,omitempty" yaml:"bootstrap-config,omitempty"`
12@@ -138,8 +139,9 @@
13 // APIEndpoint implements EnvironInfo.APIEndpoint.
14 func (info *environInfo) APIEndpoint() APIEndpoint {
15 return APIEndpoint{
16- Addresses: info.EnvInfo.StateServers,
17- CACert: info.EnvInfo.CACert,
18+ Addresses: info.EnvInfo.StateServers,
19+ CACert: info.EnvInfo.CACert,
20+ EnvironUUID: info.EnvInfo.EnvironUUID,
21 }
22 }
23
24@@ -155,6 +157,7 @@
25 func (info *environInfo) SetAPIEndpoint(endpoint APIEndpoint) {
26 info.EnvInfo.StateServers = endpoint.Addresses
27 info.EnvInfo.CACert = endpoint.CACert
28+ info.EnvInfo.EnvironUUID = endpoint.EnvironUUID
29 }
30
31 // SetAPICredentials implements EnvironInfo.SetAPICredentials.
32
33=== modified file 'environs/configstore/interface.go'
34--- environs/configstore/interface.go 2014-05-22 00:48:08 +0000
35+++ environs/configstore/interface.go 2014-05-27 09:53:17 +0000
36@@ -19,6 +19,10 @@
37 // CACert holds the CA certificate that
38 // signed the API server's key.
39 CACert string
40+
41+ // EnvironUUID holds the UUID for the environment we are connecting to.
42+ // This may be empty if the environment has not been bootstrapped.
43+ EnvironUUID string
44 }
45
46 // APICredentials hold credentials for connecting to an API endpoint.
47
48=== modified file 'environs/configstore/interface_test.go'
49--- environs/configstore/interface_test.go 2014-05-20 04:27:02 +0000
50+++ environs/configstore/interface_test.go 2014-05-27 09:53:17 +0000
51@@ -46,8 +46,9 @@
52 c.Assert(err, gc.IsNil)
53
54 expectEndpoint := configstore.APIEndpoint{
55- Addresses: []string{"example.com"},
56- CACert: "a cert",
57+ Addresses: []string{"example.com"},
58+ CACert: "a cert",
59+ EnvironUUID: "dead-beef",
60 }
61 info.SetAPIEndpoint(expectEndpoint)
62 c.Assert(info.APIEndpoint(), gc.DeepEquals, expectEndpoint)
63@@ -75,8 +76,9 @@
64 info.SetAPICredentials(expectCreds)
65
66 expectEndpoint := configstore.APIEndpoint{
67- Addresses: []string{"example.com"},
68- CACert: "a cert",
69+ Addresses: []string{"example.invalid"},
70+ CACert: "a cert",
71+ EnvironUUID: "dead-beef",
72 }
73 info.SetAPIEndpoint(expectEndpoint)
74
75
76=== modified file 'juju/api.go'
77--- juju/api.go 2014-05-13 04:30:48 +0000
78+++ juju/api.go 2014-05-27 09:53:17 +0000
79@@ -32,6 +32,7 @@
80 type apiState interface {
81 Close() error
82 APIHostPorts() [][]instance.HostPort
83+ EnvironTag() string
84 }
85
86 type apiOpenFunc func(*api.Info, api.DialOpts) (apiState, error)
87@@ -212,7 +213,7 @@
88
89 st := val0.(apiState)
90 // Even though we are about to update API addresses based on
91- // APIHostPorts in cacheChangedAPIAddresses, we first cache the
92+ // APIHostPorts in cacheChangedAPIInfo, we first cache the
93 // addresses based on the provider lookup. This is because older API
94 // servers didn't return their HostPort information on Login, and we
95 // still want to cache our connection information to them.
96@@ -231,7 +232,7 @@
97 }
98 }
99 // Update API addresses if they've changed. Error is non-fatal.
100- if localerr := cacheChangedAPIAddresses(info, st); localerr != nil {
101+ if localerr := cacheChangedAPIInfo(info, st); localerr != nil {
102 logger.Warningf("cannot failed to cache API addresses: %v", localerr)
103 }
104 return st, nil
105@@ -267,11 +268,16 @@
106 return nil, &infoConnectError{fmt.Errorf("no cached addresses")}
107 }
108 logger.Infof("connecting to API addresses: %v", endpoint.Addresses)
109+ environUUID := ""
110+ if endpoint.EnvironUUID != "" {
111+ environUUID = names.EnvironTag(endpoint.EnvironUUID)
112+ }
113 apiInfo := &api.Info{
114- Addrs: endpoint.Addresses,
115- CACert: endpoint.CACert,
116- Tag: names.UserTag(info.APICredentials().User),
117- Password: info.APICredentials().Password,
118+ Addrs: endpoint.Addresses,
119+ CACert: endpoint.CACert,
120+ Tag: names.UserTag(info.APICredentials().User),
121+ Password: info.APICredentials().Password,
122+ EnvironTag: environUUID,
123 }
124 st, err := apiOpen(apiInfo, api.DefaultDialOpts())
125 if err != nil {
126@@ -344,9 +350,18 @@
127 // with the provided apiInfo, assuming we've just successfully
128 // connected to the API server.
129 func cacheAPIInfo(info configstore.EnvironInfo, apiInfo *api.Info) error {
130+ environUUID := ""
131+ if apiInfo.EnvironTag != "" {
132+ var err error
133+ _, environUUID, err = names.ParseTag(apiInfo.Tag, names.EnvironTagKind)
134+ if err != nil {
135+ return fmt.Errorf("invalid API environment tag: %v", err)
136+ }
137+ }
138 info.SetAPIEndpoint(configstore.APIEndpoint{
139- Addresses: apiInfo.Addrs,
140- CACert: string(apiInfo.CACert),
141+ Addresses: apiInfo.Addrs,
142+ CACert: string(apiInfo.CACert),
143+ EnvironUUID: environUUID,
144 })
145 _, username, err := names.ParseTag(apiInfo.Tag, names.UserTagKind)
146 if err != nil {
147@@ -359,9 +374,10 @@
148 return info.Write()
149 }
150
151-// cacheChangedAPIAddresses updates the local environment settings (.jenv file)
152-// with the provided API server addresses if they have changed.
153-func cacheChangedAPIAddresses(info configstore.EnvironInfo, st apiState) error {
154+// cacheChangedAPIInfo updates the local environment settings (.jenv file)
155+// with the provided API server addresses if they have changed. It will also
156+// save the environment tag if it is available.
157+func cacheChangedAPIInfo(info configstore.EnvironInfo, st apiState) error {
158 var addrs []string
159 for _, serverHostPorts := range st.APIHostPorts() {
160 for _, hostPort := range serverHostPorts {
161@@ -373,11 +389,23 @@
162 }
163 }
164 endpoint := info.APIEndpoint()
165- if len(addrs) == 0 || !addrsChanged(endpoint.Addresses, addrs) {
166+ newEnvironTag := st.EnvironTag()
167+ changed := false
168+ if newEnvironTag != "" {
169+ _, environUUID, err := names.ParseTag(newEnvironTag, names.EnvironTagKind)
170+ if err == nil && endpoint.EnvironUUID != environUUID {
171+ changed = true
172+ endpoint.EnvironUUID = environUUID
173+ }
174+ }
175+ if len(addrs) != 0 && addrsChanged(endpoint.Addresses, addrs) {
176+ logger.Debugf("API addresses changed from %q to %q", endpoint.Addresses, addrs)
177+ changed = true
178+ endpoint.Addresses = addrs
179+ }
180+ if !changed {
181 return nil
182 }
183- logger.Debugf("API addresses changed from %q to %q", endpoint.Addresses, addrs)
184- endpoint.Addresses = addrs
185 info.SetAPIEndpoint(endpoint)
186 if err := info.Write(); err != nil {
187 return err
188
189=== modified file 'juju/apiconn_test.go'
190--- juju/apiconn_test.go 2014-05-16 01:33:13 +0000
191+++ juju/apiconn_test.go 2014-05-27 09:53:17 +0000
192@@ -123,11 +123,13 @@
193 apiHostPorts: [][]instance.HostPort{
194 instance.AddressesWithPort([]instance.Address{instance.NewAddress("0.1.2.3", instance.NetworkUnknown)}, 1234),
195 },
196+ environTag: "environment-fake-uuid",
197 }
198 apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) {
199 c.Check(apiInfo.Tag, gc.Equals, "user-foo")
200 c.Check(string(apiInfo.CACert), gc.Equals, "certificated")
201 c.Check(apiInfo.Password, gc.Equals, "foopass")
202+ c.Check(apiInfo.EnvironTag, gc.Equals, "environment-fake-uuid")
203 c.Check(opts, gc.DeepEquals, api.DefaultDialOpts())
204 called++
205 return expectState, nil
206@@ -143,7 +145,9 @@
207 c.Assert(mockStore.written, jc.IsTrue)
208 info, err := store.ReadInfo("noconfig")
209 c.Assert(err, gc.IsNil)
210- c.Assert(info.APIEndpoint().Addresses, gc.DeepEquals, []string{"0.1.2.3:1234"})
211+ ep := info.APIEndpoint()
212+ c.Assert(ep.Addresses, gc.DeepEquals, []string{"0.1.2.3:1234"})
213+ c.Check(ep.EnvironUUID, gc.Equals, "fake-uuid")
214 mockStore.written = false
215
216 // If APIHostPorts haven't changed, then the store won't be updated.
217@@ -185,6 +189,8 @@
218 c.Check(apiInfo.Tag, gc.Equals, "user-admin")
219 c.Check(string(apiInfo.CACert), gc.Not(gc.Equals), "")
220 c.Check(apiInfo.Password, gc.Equals, "adminpass")
221+ // EnvironTag wasn't in regular Config
222+ c.Check(apiInfo.EnvironTag, gc.Equals, "")
223 c.Check(opts, gc.DeepEquals, api.DefaultDialOpts())
224 called++
225 return expectState, nil
226@@ -202,6 +208,9 @@
227 c.Assert(ep.Addresses, gc.HasLen, 1)
228 c.Check(ep.Addresses[0], gc.Matches, `127\.0\.0\.1:\d+`)
229 c.Check(ep.CACert, gc.Not(gc.Equals), "")
230+ // Old servers won't hand back EnvironTag, so it should stay empty in
231+ // the cache
232+ c.Check(ep.EnvironUUID, gc.Equals, "")
233 creds := info.APICredentials()
234 c.Check(creds.User, gc.Equals, "admin")
235 c.Check(creds.Password, gc.Equals, "adminpass")
236@@ -227,6 +236,128 @@
237 c.Assert(st, gc.IsNil)
238 }
239
240+func (s *NewAPIClientSuite) TestWithInfoNoEnvironTag(c *gc.C) {
241+ store := newConfigStore("noconfig", &environInfo{
242+ creds: configstore.APICredentials{
243+ User: "foo",
244+ Password: "foopass",
245+ },
246+ endpoint: configstore.APIEndpoint{
247+ Addresses: []string{"foo.invalid"},
248+ CACert: "certificated",
249+ },
250+ })
251+
252+ called := 0
253+ expectState := &mockAPIState{
254+ apiHostPorts: [][]instance.HostPort{
255+ instance.AddressesWithPort([]instance.Address{instance.NewAddress("0.1.2.3", instance.NetworkUnknown)}, 1234),
256+ },
257+ environTag: "environment-fake-uuid",
258+ }
259+ apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) {
260+ c.Check(apiInfo.Tag, gc.Equals, "user-foo")
261+ c.Check(string(apiInfo.CACert), gc.Equals, "certificated")
262+ c.Check(apiInfo.Password, gc.Equals, "foopass")
263+ c.Check(apiInfo.EnvironTag, gc.Equals, "")
264+ c.Check(opts, gc.DeepEquals, api.DefaultDialOpts())
265+ called++
266+ return expectState, nil
267+ }
268+
269+ // Give NewAPIFromStore a store interface that can report when the
270+ // config was written to, to check if the cache is updated.
271+ mockStore := &storageWithWriteNotify{store: store}
272+ st, err := juju.NewAPIFromStore("noconfig", mockStore, apiOpen)
273+ c.Assert(err, gc.IsNil)
274+ c.Assert(st, gc.Equals, expectState)
275+ c.Assert(called, gc.Equals, 1)
276+ c.Assert(mockStore.written, jc.IsTrue)
277+ info, err := store.ReadInfo("noconfig")
278+ c.Assert(err, gc.IsNil)
279+ c.Assert(info.APIEndpoint().Addresses, gc.DeepEquals, []string{"0.1.2.3:1234"})
280+ c.Check(info.APIEndpoint().EnvironUUID, gc.Equals, "fake-uuid")
281+}
282+
283+func (s *NewAPIClientSuite) TestWithInfoNoAPIHostports(c *gc.C) {
284+ // The local cache doesn't have an EnvironTag, which the API does
285+ // return. However, the API doesn't have apiHostPorts, we don't want to
286+ // override the local cache with bad endpoints.
287+ store := newConfigStore("noconfig", &environInfo{
288+ creds: configstore.APICredentials{
289+ User: "foo",
290+ Password: "foopass",
291+ },
292+ endpoint: configstore.APIEndpoint{
293+ Addresses: []string{"foo.invalid"},
294+ CACert: "certificated",
295+ },
296+ })
297+
298+ called := 0
299+ expectState := &mockAPIState{
300+ apiHostPorts: [][]instance.HostPort{},
301+ environTag: "environment-fake-uuid",
302+ }
303+ apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) {
304+ c.Check(apiInfo.Tag, gc.Equals, "user-foo")
305+ c.Check(string(apiInfo.CACert), gc.Equals, "certificated")
306+ c.Check(apiInfo.Password, gc.Equals, "foopass")
307+ c.Check(apiInfo.EnvironTag, gc.Equals, "")
308+ c.Check(opts, gc.DeepEquals, api.DefaultDialOpts())
309+ called++
310+ return expectState, nil
311+ }
312+
313+ mockStore := &storageWithWriteNotify{store: store}
314+ st, err := juju.NewAPIFromStore("noconfig", mockStore, apiOpen)
315+ c.Assert(err, gc.IsNil)
316+ c.Assert(st, gc.Equals, expectState)
317+ c.Assert(called, gc.Equals, 1)
318+ c.Assert(mockStore.written, jc.IsTrue)
319+ info, err := store.ReadInfo("noconfig")
320+ c.Assert(err, gc.IsNil)
321+ ep := info.APIEndpoint()
322+ // We should have cached the environ tag, but not disturbed the
323+ // Addresses
324+ c.Check(ep.Addresses, gc.HasLen, 1)
325+ c.Check(ep.Addresses[0], gc.Matches, `foo\.invalid`)
326+ c.Check(ep.EnvironUUID, gc.Equals, "fake-uuid")
327+}
328+
329+func (s *NewAPIClientSuite) TestNoEnvironTagDoesntOverwriteCached(c *gc.C) {
330+ store := newConfigStore("noconfig", dummyStoreInfo)
331+ called := 0
332+ // State returns a new set of APIHostPorts but not a new EnvironTag. We
333+ // shouldn't override the cached value with environ tag of "".
334+ expectState := &mockAPIState{
335+ apiHostPorts: [][]instance.HostPort{
336+ instance.AddressesWithPort([]instance.Address{instance.NewAddress("0.1.2.3", instance.NetworkUnknown)}, 1234),
337+ },
338+ }
339+ apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) {
340+ c.Check(apiInfo.Tag, gc.Equals, "user-foo")
341+ c.Check(string(apiInfo.CACert), gc.Equals, "certificated")
342+ c.Check(apiInfo.Password, gc.Equals, "foopass")
343+ c.Check(apiInfo.EnvironTag, gc.Equals, "environment-fake-uuid")
344+ c.Check(opts, gc.DeepEquals, api.DefaultDialOpts())
345+ called++
346+ return expectState, nil
347+ }
348+
349+ mockStore := &storageWithWriteNotify{store: store}
350+ st, err := juju.NewAPIFromStore("noconfig", mockStore, apiOpen)
351+ c.Assert(err, gc.IsNil)
352+ c.Assert(st, gc.Equals, expectState)
353+ c.Assert(called, gc.Equals, 1)
354+ c.Assert(mockStore.written, jc.IsTrue)
355+ info, err := store.ReadInfo("noconfig")
356+ c.Assert(err, gc.IsNil)
357+ ep := info.APIEndpoint()
358+ c.Assert(ep.Addresses, gc.DeepEquals, []string{"0.1.2.3:1234"})
359+ c.Check(ep.EnvironUUID, gc.Equals, "fake-uuid")
360+}
361+
362 func (s *NewAPIClientSuite) TestWithInfoAPIOpenError(c *gc.C) {
363 store := newConfigStore("noconfig", &environInfo{
364 endpoint: configstore.APIEndpoint{
365@@ -584,8 +715,9 @@
366 Password: "foopass",
367 },
368 endpoint: configstore.APIEndpoint{
369- Addresses: []string{"foo.invalid"},
370- CACert: "certificated",
371+ Addresses: []string{"foo.invalid"},
372+ CACert: "certificated",
373+ EnvironUUID: "fake-uuid",
374 },
375 }
376
377@@ -631,12 +763,15 @@
378 apiHostPorts: [][]instance.HostPort{
379 instance.AddressesWithPort([]instance.Address{instance.NewAddress("0.1.2.3", instance.NetworkUnknown)}, 1234),
380 },
381+ environTag: "environment-fake-uuid",
382 }
383 apiOpen := func(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) {
384 c.Check(apiInfo.Tag, gc.Equals, "user-admin")
385 c.Check(string(apiInfo.CACert), gc.Equals, coretesting.CACert)
386 c.Check(apiInfo.Password, gc.Equals, coretesting.DefaultMongoPassword)
387 c.Check(opts, gc.DeepEquals, api.DefaultDialOpts())
388+ // we didn't know about it when connecting
389+ c.Check(apiInfo.EnvironTag, gc.Equals, "")
390 called++
391 return expectState, nil
392 }
393@@ -644,6 +779,7 @@
394 c.Assert(err, gc.IsNil)
395 c.Assert(called, gc.Equals, 1)
396 c.Check(endpoint.Addresses, gc.DeepEquals, []string{"0.1.2.3:1234"})
397+ c.Check(endpoint.EnvironUUID, gc.Equals, "fake-uuid")
398 }
399
400 func (s *APIEndpointForEnvSuite) TestAPIEndpointRefresh(c *gc.C) {
401
402=== modified file 'juju/mock_test.go'
403--- juju/mock_test.go 2014-03-31 12:24:52 +0000
404+++ juju/mock_test.go 2014-05-27 09:53:17 +0000
405@@ -10,6 +10,7 @@
406 close func(juju.APIState) error
407
408 apiHostPorts [][]instance.HostPort
409+ environTag string
410 }
411
412 func (s *mockAPIState) Close() error {
413@@ -23,6 +24,10 @@
414 return s.apiHostPorts
415 }
416
417+func (s *mockAPIState) EnvironTag() string {
418+ return s.environTag
419+}
420+
421 func panicAPIOpen(apiInfo *api.Info, opts api.DialOpts) (juju.APIState, error) {
422 panic("api.Open called unexpectedly")
423 }
424
425=== modified file 'state/api/apiclient.go'
426--- state/api/apiclient.go 2014-04-30 23:18:40 +0000
427+++ state/api/apiclient.go 2014-05-27 09:53:17 +0000
428@@ -35,6 +35,9 @@
429 // addr is the address used to connect to the API server.
430 addr string
431
432+ // environTag holds the environment tag once we're connected
433+ environTag string
434+
435 // hostPorts is the API server addresses returned from Login,
436 // which the client may cache and use for failover.
437 hostPorts [][]instance.HostPort
438@@ -80,6 +83,10 @@
439 // Nonce holds the nonce used when provisioning the machine. Used
440 // only by the machine agent.
441 Nonce string `yaml:",omitempty"`
442+
443+ // Environ holds the environ tag for the environment we are trying to
444+ // connect to.
445+ EnvironTag string
446 }
447
448 // DialOpts holds configuration parameters that control the
449@@ -155,7 +162,7 @@
450 certPool: pool,
451 }
452 if info.Tag != "" || info.Password != "" {
453- if err := st.Login(info.Tag, info.Password, info.Nonce); err != nil {
454+ if err := st.Login(info.Tag, info.Password, info.Nonce, info.EnvironTag); err != nil {
455 conn.Close()
456 return nil, err
457 }
458@@ -261,6 +268,12 @@
459 return s.addr
460 }
461
462+// EnvironTag returns the Environment Tag describing the environment we are
463+// connected to.
464+func (s *State) EnvironTag() string {
465+ return s.environTag
466+}
467+
468 // APIHostPorts returns addresses that may be used to connect
469 // to the API server, including the address used to connect.
470 //
471
472=== modified file 'state/api/apiclient_test.go'
473--- state/api/apiclient_test.go 2014-04-02 09:43:39 +0000
474+++ state/api/apiclient_test.go 2014-05-27 09:53:17 +0000
475@@ -11,6 +11,7 @@
476
477 jujutesting "launchpad.net/juju-core/juju/testing"
478 "launchpad.net/juju-core/state/api"
479+ "launchpad.net/juju-core/state/api/params"
480 "launchpad.net/juju-core/utils/parallel"
481 )
482
483@@ -79,6 +80,27 @@
484 c.Assert(err, gc.ErrorMatches, `timed out connecting to "wss://.*/"`)
485 }
486
487+func (s *apiclientSuite) TestOpenPassesEnvironTag(c *gc.C) {
488+ info := s.APIInfo(c)
489+ environTag := info.EnvironTag
490+ env, err := s.State.Environment()
491+ c.Assert(err, gc.IsNil)
492+ c.ExpectFailure("JujuConnSuite.APIInfo() uses APIConn which doesn't know about EnvironTag")
493+ c.Check(environTag, gc.Equals, env.Tag())
494+ c.Assert(environTag, gc.Not(gc.Equals), "")
495+ // We start by ensuring we have an invalid tag, and Open should fail.
496+ info.EnvironTag = "environ-bad-tag"
497+ _, err = api.Open(info, api.DialOpts{})
498+ c.Check(err, gc.ErrorMatches, "invalid environment requested")
499+ c.Check(params.ErrCode(err), gc.Equals, params.CodeUnauthorized)
500+ c.Assert(err, gc.IsNil)
501+ // Now set it to the right tag, and we should succeed.
502+ info.EnvironTag = env.Tag()
503+ st, err := api.Open(info, api.DialOpts{})
504+ c.Assert(err, gc.IsNil)
505+ st.Close()
506+}
507+
508 func (s *apiclientSuite) TestDialWebsocketStopped(c *gc.C) {
509 stopped := make(chan struct{})
510 f := api.NewWebsocketDialer(nil, api.DialOpts{})
511
512=== modified file 'state/api/params/params.go'
513--- state/api/params/params.go 2014-05-27 08:30:44 +0000
514+++ state/api/params/params.go 2014-05-27 09:53:17 +0000
515@@ -293,9 +293,10 @@
516
517 // Creds holds credentials for identifying an entity.
518 type Creds struct {
519- AuthTag string
520- Password string
521- Nonce string
522+ AuthTag string
523+ Password string
524+ Nonce string
525+ EnvironTag string `json:",omitempty"`
526 }
527
528 // GetAnnotationsResults holds annotations associated with an entity.
529@@ -719,7 +720,8 @@
530
531 // LoginResult holds the result of a Login call.
532 type LoginResult struct {
533- Servers [][]instance.HostPort
534+ Servers [][]instance.HostPort
535+ EnvironTag string
536 }
537
538 // EnsureAvailability contains arguments for
539
540=== modified file 'state/api/state.go'
541--- state/api/state.go 2014-04-16 12:55:56 +0000
542+++ state/api/state.go 2014-05-27 09:53:17 +0000
543@@ -27,12 +27,13 @@
544 // Subsequent requests on the state will act as that entity. This
545 // method is usually called automatically by Open. The machine nonce
546 // should be empty unless logging in as a machine agent.
547-func (st *State) Login(tag, password, nonce string) error {
548+func (st *State) Login(tag, password, nonce, environTag string) error {
549 var result params.LoginResult
550 err := st.Call("Admin", "", "Login", &params.Creds{
551- AuthTag: tag,
552- Password: password,
553- Nonce: nonce,
554+ AuthTag: tag,
555+ Password: password,
556+ Nonce: nonce,
557+ EnvironTag: environTag,
558 }, &result)
559 if err == nil {
560 st.authTag = tag
561@@ -42,6 +43,7 @@
562 return err
563 }
564 st.hostPorts = hostPorts
565+ st.environTag = result.EnvironTag
566 }
567 return err
568 }
569
570=== modified file 'state/api/state_test.go'
571--- state/api/state_test.go 2014-05-20 04:27:02 +0000
572+++ state/api/state_test.go 2014-05-27 09:53:17 +0000
573@@ -11,6 +11,7 @@
574 "launchpad.net/juju-core/instance"
575 jujutesting "launchpad.net/juju-core/juju/testing"
576 "launchpad.net/juju-core/state/api"
577+ "launchpad.net/juju-core/state/api/params"
578 coretesting "launchpad.net/juju-core/testing"
579 )
580
581@@ -54,10 +55,56 @@
582 s.State.SetAPIHostPorts([][]instance.HostPort{badServer})
583 apistate, err := api.Open(info, api.DialOpts{})
584 c.Assert(err, gc.IsNil)
585+ defer apistate.Close()
586 hostports := apistate.APIHostPorts()
587 c.Check(hostports, gc.DeepEquals, [][]instance.HostPort{serverhostports, badServer})
588 }
589
590+func (s *stateSuite) TestLoginSetsEnvironTag(c *gc.C) {
591+ env, err := s.State.Environment()
592+ c.Assert(err, gc.IsNil)
593+ info := s.APIInfo(c)
594+ tag := info.Tag
595+ password := info.Password
596+ info.Tag = ""
597+ info.Password = ""
598+ apistate, err := api.Open(info, api.DialOpts{})
599+ c.Assert(err, gc.IsNil)
600+ defer apistate.Close()
601+ // We haven't called Login yet, so the EnvironTag shouldn't be set.
602+ c.Check(apistate.EnvironTag(), gc.Equals, "")
603+ err = apistate.Login(tag, password, "", "")
604+ c.Assert(err, gc.IsNil)
605+ // Now that we've logged in, EnvironTag should be updated correctly.
606+ c.Check(apistate.EnvironTag(), gc.Equals, env.Tag())
607+}
608+
609+func (s *stateSuite) TestLoginPassesEnvironTag(c *gc.C) {
610+ env, err := s.State.Environment()
611+ c.Assert(err, gc.IsNil)
612+ info := s.APIInfo(c)
613+ tag := info.Tag
614+ password := info.Password
615+ info.Tag = ""
616+ info.Password = ""
617+ apistate, err := api.Open(info, api.DialOpts{})
618+ c.Assert(err, gc.IsNil)
619+ defer apistate.Close()
620+ // Not set yet.
621+ c.Check(apistate.EnvironTag(), gc.Equals, "")
622+ // We intentionally pass in an invalid environ tag, and notice that
623+ // Login is refused. We then pass in the right tag, to make sure we
624+ // have the right information
625+ err = apistate.Login(tag, password, "", "environment-not-uuid")
626+ c.Check(err, gc.ErrorMatches, "invalid environment requested")
627+ c.Check(params.ErrCode(err), gc.Equals, params.CodeUnauthorized)
628+ c.Check(apistate.EnvironTag(), gc.Equals, "")
629+ err = apistate.Login(tag, password, "", env.Tag())
630+ c.Assert(err, gc.IsNil)
631+ // Now that we've logged in, EnvironTag should be updated correctly.
632+ c.Check(apistate.EnvironTag(), gc.Equals, env.Tag())
633+}
634+
635 func (s *stateSuite) TestAPIHostPortsMovesConnectedValueFirst(c *gc.C) {
636 hostportslist := s.APIState.APIHostPorts()
637 c.Check(hostportslist, gc.HasLen, 1)
638@@ -91,6 +138,7 @@
639 s.State.SetAPIHostPorts(current)
640 apistate, err := api.Open(info, api.DialOpts{})
641 c.Assert(err, gc.IsNil)
642+ defer apistate.Close()
643 hostports := apistate.APIHostPorts()
644 // We should have rotate the server we connected to as the first item,
645 // and the address of that server as the first address
646
647=== modified file 'state/apiserver/admin.go'
648--- state/apiserver/admin.go 2014-05-20 08:02:01 +0000
649+++ state/apiserver/admin.go 2014-05-27 09:53:17 +0000
650@@ -105,8 +105,16 @@
651 }
652 logger.Debugf("hostPorts: %v", hostPorts)
653
654+ environ, err := a.root.srv.state.Environment()
655+ if err != nil {
656+ return params.LoginResult{}, err
657+ }
658+
659 a.root.rpcConn.Serve(newRoot, serverError)
660- return params.LoginResult{hostPorts}, nil
661+ return params.LoginResult{
662+ Servers: hostPorts,
663+ EnvironTag: environ.Tag(),
664+ }, nil
665 }
666
667 var doCheckCreds = checkCreds
668@@ -127,6 +135,18 @@
669 if err != nil || !entity.PasswordValid(c.Password) {
670 return nil, common.ErrBadCreds
671 }
672+ if c.EnvironTag != "" {
673+ env, err := st.Environment()
674+ if err != nil {
675+ // TODO: There is no test for this case, how do we even
676+ // trigger an error for the Environment() call?
677+ logger.Warningf("got error asking for state.Environment(): %v", err)
678+ return nil, err
679+ }
680+ if env.Tag() != c.EnvironTag {
681+ return nil, common.ErrInvalidEnviron
682+ }
683+ }
684 // Check if a machine agent is logging in with the right Nonce
685 if err := checkForValidMachineAgent(entity, c); err != nil {
686 return nil, err
687
688=== modified file 'state/apiserver/common/errors.go'
689--- state/apiserver/common/errors.go 2014-05-13 04:30:48 +0000
690+++ state/apiserver/common/errors.go 2014-05-27 09:53:17 +0000
691@@ -47,6 +47,7 @@
692 var (
693 ErrBadId = stderrors.New("id not found")
694 ErrBadCreds = stderrors.New("invalid entity name or password")
695+ ErrInvalidEnviron = stderrors.New("invalid environment requested")
696 ErrPerm = stderrors.New("permission denied")
697 ErrNotLoggedIn = stderrors.New("not logged in")
698 ErrUnknownWatcher = stderrors.New("unknown watcher id")
699@@ -63,6 +64,7 @@
700 state.ErrUnitHasSubordinates: params.CodeUnitHasSubordinates,
701 ErrBadId: params.CodeNotFound,
702 ErrBadCreds: params.CodeUnauthorized,
703+ ErrInvalidEnviron: params.CodeUnauthorized,
704 ErrPerm: params.CodeUnauthorized,
705 ErrNotLoggedIn: params.CodeUnauthorized,
706 ErrUnknownWatcher: params.CodeNotFound,
707
708=== modified file 'state/apiserver/login_test.go'
709--- state/apiserver/login_test.go 2014-04-17 13:14:49 +0000
710+++ state/apiserver/login_test.go 2014-05-27 09:53:17 +0000
711@@ -33,6 +33,7 @@
712 tag string
713 password string
714 err string
715+ envtag string
716 code string
717 }{{
718 tag: "user-admin",
719@@ -48,6 +49,12 @@
720 tag: "bar",
721 password: "password",
722 err: `"bar" is not a valid tag`,
723+}, {
724+ tag: "user-admin",
725+ password: "dummy-secret",
726+ envtag: "environment-bad-uuid",
727+ err: "invalid environment requested",
728+ code: params.CodeUnauthorized,
729 }}
730
731 func (s *loginSuite) setupServer(c *gc.C) (*api.Info, func()) {
732@@ -59,11 +66,14 @@
733 "", "",
734 )
735 c.Assert(err, gc.IsNil)
736+ env, err := s.State.Environment()
737+ c.Assert(err, gc.IsNil)
738 info := &api.Info{
739- Tag: "",
740- Password: "",
741- Addrs: []string{srv.Addr()},
742- CACert: coretesting.CACert,
743+ Tag: "",
744+ Password: "",
745+ EnvironTag: env.Tag(),
746+ Addrs: []string{srv.Addr()},
747+ CACert: coretesting.CACert,
748 }
749 return info, func() {
750 err := srv.Stop()
751@@ -111,7 +121,7 @@
752 c.Assert(err, gc.ErrorMatches, `unknown object type "Machiner"`)
753
754 // Since these are user login tests, the nonce is empty.
755- err = st.Login(t.tag, t.password, "")
756+ err = st.Login(t.tag, t.password, "", t.envtag)
757 c.Assert(err, gc.ErrorMatches, t.err)
758 c.Assert(params.ErrCode(err), gc.Equals, t.code)
759
760@@ -139,7 +149,7 @@
761 c.Assert(err, gc.ErrorMatches, `unknown object type "Client"`)
762
763 // Since these are user login tests, the nonce is empty.
764- err = st.Login("user-inactive", "password", "")
765+ err = st.Login("user-inactive", "password", "", info.EnvironTag)
766 c.Assert(err, gc.ErrorMatches, "invalid entity name or password")
767
768 _, err = st.Client().Status([]string{})
769@@ -177,12 +187,12 @@
770
771 c.Assert(tw.Log, jc.LogMatches, []string{
772 `<- \[[0-9A-F]+\] <unknown> {"RequestId":1,"Type":"Admin","Request":"Login","Params":` +
773- `{"AuthTag":"machine-0","Password":"[^"]*","Nonce":"fake_nonce"}` +
774+ `{"AuthTag":"machine-0","Password":"[^"]*","Nonce":"fake_nonce","EnvironTag":"environ[^"]*"}` +
775 `}`,
776 // Now that we are logged in, we see the entity's tag
777 // [0-9.umns] is to handle timestamps that are ns, us, ms, or s
778 // long, though we expect it to be in the 'ms' range.
779- `-> \[[0-9A-F]+\] machine-0 [0-9.]+[umn]?s {"RequestId":1,"Response":{"Servers":\[\]}} Admin\[""\].Login`,
780+ `-> \[[0-9A-F]+\] machine-0 [0-9.]+[umn]?s {"RequestId":1,"Response":.*} Admin\[""\].Login`,
781 `<- \[[0-9A-F]+\] machine-0 {"RequestId":2,"Type":"Machiner","Request":"Life","Params":{"Entities":\[{"Tag":"machine-0"}\]}}`,
782 `-> \[[0-9A-F]+\] machine-0 [0-9.umns]+ {"RequestId":2,"Response":{"Results":\[{"Life":"alive","Error":null}\]}} Machiner\[""\]\.Life`,
783 })
784@@ -461,3 +471,67 @@
785 c.Check(err, gc.IsNil)
786 }
787 }
788+
789+func (s *loginSuite) TestLoginReportsEnvironTag(c *gc.C) {
790+ info, cleanup := s.setupServer(c)
791+ defer cleanup()
792+ // If we call api.Open without giving a username and password, then it
793+ // won't call Login, so we can call it ourselves.
794+ // We Login without passing an EnvironTag, to show that it still lets
795+ // us in, and that we can find out the real EnvironTag from the
796+ // response.
797+ st, err := api.Open(info, fastDialOpts)
798+ c.Assert(err, gc.IsNil)
799+ var result params.LoginResult
800+ creds := &params.Creds{
801+ AuthTag: "user-admin",
802+ Password: "dummy-secret",
803+ EnvironTag: "",
804+ }
805+ err = st.Call("Admin", "", "Login", creds, &result)
806+ c.Assert(err, gc.IsNil)
807+ env, err := s.State.Environment()
808+ c.Assert(err, gc.IsNil)
809+ c.Assert(result.EnvironTag, gc.Equals, env.Tag())
810+}
811+
812+func (s *loginSuite) TestLoginAcceptsEnvironTag(c *gc.C) {
813+ info, cleanup := s.setupServer(c)
814+ defer cleanup()
815+ // We open the API without logging in, and then do a direct login
816+ // passing in the EnvironTag. The server should let us in, and we
817+ // should be able to check from the response that we are, indeed, in
818+ // the right environment.
819+ st, err := api.Open(info, fastDialOpts)
820+ c.Assert(err, gc.IsNil)
821+ env, err := s.State.Environment()
822+ c.Assert(err, gc.IsNil)
823+ var result params.LoginResult
824+ creds := &params.Creds{
825+ AuthTag: "user-admin",
826+ Password: "dummy-secret",
827+ EnvironTag: env.Tag(),
828+ }
829+ err = st.Call("Admin", "", "Login", creds, &result)
830+ c.Assert(err, gc.IsNil)
831+ c.Assert(result.EnvironTag, gc.Equals, env.Tag())
832+}
833+
834+func (s *loginSuite) TestLoginRefusesBadEnvironTag(c *gc.C) {
835+ info, cleanup := s.setupServer(c)
836+ defer cleanup()
837+ // This time, we pass in an environment tag that doesn't actually match
838+ // the environment. The login should be rejected with an appropriate
839+ // error response.
840+ st, err := api.Open(info, fastDialOpts)
841+ c.Assert(err, gc.IsNil)
842+ var result params.LoginResult
843+ creds := &params.Creds{
844+ AuthTag: "user-admin",
845+ Password: "dummy-secret",
846+ EnvironTag: "environment-not-my-uuid",
847+ }
848+ err = st.Call("Admin", "", "Login", creds, &result)
849+ c.Assert(err, gc.ErrorMatches, "invalid environment requested")
850+ c.Assert(params.ErrCode(err), gc.Equals, params.CodeUnauthorized)
851+}

Subscribers

People subscribed via source and target branches

to status/vote changes: