Merge lp:~axwalk/juju-core/state-environment-life into lp:~go-bot/juju-core/trunk
- state-environment-life
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Andrew Wilkins |
Approved revision: | no longer in the source branch. |
Merged at revision: | 2131 |
Proposed branch: | lp:~axwalk/juju-core/state-environment-life |
Merge into: | lp:~go-bot/juju-core/trunk |
Diff against target: |
604 lines (+298/-39) 11 files modified
names/environ.go (+9/-7) state/addmachine.go (+14/-0) state/cleanup.go (+22/-0) state/cleanup_test.go (+42/-0) state/environ.go (+69/-13) state/environ_test.go (+1/-3) state/initialize_test.go (+9/-4) state/interface.go (+1/-0) state/state.go (+34/-6) state/state_test.go (+92/-6) state/watcher.go (+5/-0) |
To merge this branch: | bzr merge lp:~axwalk/juju-core/state-environment-life |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+195710@code.launchpad.net |
Commit message
state: add Life to state.Environment
Environment has been given new methods
to mark an environment as dying, and
to remove the environment from state.
An environment must now exist in state
and be Alive for services or machines
to be added to state.
A secondary change here is to use the
environment's UUID in the tag, rather
than the environment's name, which is
non-unique. We continue to accept the
name as input, until we know all users
are passing in UUIDs.
Description of the change
state: add Life to state.Environment
Environment has been given new methods
to mark an environment as dying, and
to remove the environment from state.
An environment must now exist in state
and be Alive for services or machines
to be added to state.
A secondary change here is to use the
environment's UUID in the tag, rather
than the environment's name, which is
non-unique. We continue to accept the
name as input, until we know all users
are passing in UUIDs.
Andrew Wilkins (axwalk) wrote : | # |
Andrew Wilkins (axwalk) wrote : | # |
On 2013/11/19 06:47:52, axw wrote:
> Please take a look.
I should have mentioned in the description: this is an offshoot of
https:/
The purpose of this CL is twofold:
- to prevent machines and services from being added while an
environment is being destroyed
- and to provide a clean-termination mechanism for state-server machine
agents
Frank Mueller (themue) wrote : | # |
LGTM, only a real minor comment.
https:/
File names/environ.go (right):
https:/
names/environ.
environment id.
id (in the comment) or name (in the code)?
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
https:/
File names/environ.go (right):
https:/
names/environ.
environment id.
On 2013/11/20 15:24:19, mue wrote:
> id (in the comment) or name (in the code)?
id, but actually, we can do that TODO now since the id is now a UUID
with a well defined format. Updated to do that, and updated tests.
William Reade (fwereade) wrote : | # |
Needs a bit more thought, I'm afraid.
https:/
File names/environ.go (right):
https:/
names/environ.
Have we checked all possible clients of this? I'm pretty sure we don't
accept environment tags ever, but we do produce them, and the GUI may
need to be prepared for this change -- please check with frankban.
https:/
File state/environ.go (right):
https:/
state/environ.
If it's already dying we should abort, too, so this is just the
assertAliveOp
https:/
state/environ.
We can't track the complete set of services transactionally, so I reckon
the next best thing is to add a cleanup job that sets all services to
dying one the environment has been set to dying.
I kinda hate this, but we're stuck with it without a schemachange for a
total-service-
SCHEMACHANGE so we have a chance of fixing it.
https:/
state/environ.
nil)
I'm not sure how meaningful this code is at this point -- *everything*
has an implicit environment reference at the moment, so I'm not when
this can be sanely called. Ofc, something like this is a necessary
component of a multi-tenant server, in the future -- but it will
certainly require that removing the environment removes everything that
references it. Thoughts? Can we maybe even just stick with alive/dying
for now?
https:/
File state/initializ
https:/
state/initializ
Find out from the GUI guys exactly what they use environment annotations
for, if anything, and whether the tag change will affect them.
https:/
File state/state.go (right):
https:/
state/state.go:359: env, err := st.Environment()
If we're going to pay the DB hit for the environment here, we should
probably check for life and abort here as well.
To avoid duplicating code, you may prefer to structure this as a loop
for i < 2.
https:/
state/state.go:616: if id != env.UUID() {
It may be that this is the only bit we need to fix -- and we should just
accept that there's only one environment ever in play, so we should just
assume that environment-
That'll work for now, at least.
https:/
state/state.go:735: return nil, err
Check for dying before running tran...
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
https:/
File names/environ.go (right):
https:/
names/environ.
On 2013/11/21 13:04:18, fwereade wrote:
> Have we checked all possible clients of this? I'm pretty sure we don't
accept
> environment tags ever, but we do produce them, and the GUI may need to
be
> prepared for this change -- please check with frankban.
Turns out that Landscape is using it, but only in IS. Have discussed
with Bjorn Tillenius, and he will update Landscape to use UUID with
fallback to name for the next version of Landscape, and we'll make the
change in the following version of Juju. After that is in IS, Landscape
can drop the backwards compatibility.
https:/
File state/environ.go (right):
https:/
state/environ.
On 2013/11/21 13:04:18, fwereade wrote:
> If it's already dying we should abort, too, so this is just the
assertAliveOp
Why should we abort if Dying? While destroying other entities aborts if
they're dying, they ultimately do not return an error to the caller.
Is there a reason to prefer aborting and then checking if Life==Dying
and returning nil? Is the no-op modification not really a no-op?
I don't have a problem doing it, but I'd rather understand why first.
https:/
state/environ.
On 2013/11/21 13:04:18, fwereade wrote:
> We can't track the complete set of services transactionally, so I
reckon the
> next best thing is to add a cleanup job that sets all services to
dying one the
> environment has been set to dying.
Okay, I'll need to add a new cleanup kind then. What about machines? I
can't use the "machine" cleanup as that implies --force, which I don't
think we want here do we?
> I kinda hate this, but we're stuck with it without a schemachange for
a
> total-service-
SCHEMACHANGE so
> we have a chance of fixing it.
Done.
https:/
state/environ.
nil)
On 2013/11/21 13:04:18, fwereade wrote:
> I'm not sure how meaningful this code is at this point -- *everything*
has an
> implicit environment reference at the moment, so I'm not when this can
be sanely
> called. Ofc, something like this is a necessary component of a
multi-tenant
> server, in the future -- but it will certainly require that removing
the
> environment removes everything that references it. Thoughts? Can we
maybe even
> just stick with alive/dying for now?
Okay, fair enough. The intention was that Remove would trigger state
termination, so there'd be nothing left anyway.
Without it we're back to having no way of signalling manager nodes to
terminate. I will have to go back to my earlier approach of "ssh <host>
pkill -SIGABRT jujud" in the manual provider, and all others will be
de...
William Reade (fwereade) wrote : | # |
Nearly there, I think.
I do feel that accepting both env-tag kinds is the best way for us to
support the transition, but maybe I'm confused about something.
https:/
File state/environ.go (right):
https:/
state/environ.
On 2013/11/28 09:42:48, axw wrote:
> On 2013/11/21 13:04:18, fwereade wrote:
> > If it's already dying we should abort, too, so this is just the
assertAliveOp
> Why should we abort if Dying? While destroying other entities aborts
if they're
> dying, they ultimately do not return an error to the caller.
> Is there a reason to prefer aborting and then checking if Life==Dying
and
> returning nil? Is the no-op modification not really a no-op?
> I don't have a problem doing it, but I'd rather understand why first.
It's just defensive -- if the transaction ever grows such that it's not
a no-op, it'll already be correct.
https:/
File state/state.go (right):
https:/
state/state.go:616: if id != env.UUID() {
On 2013/11/28 09:42:48, axw wrote:
> On 2013/11/21 13:04:18, fwereade wrote:
> > It may be that this is the only bit we need to fix -- and we should
just
> accept
> > that there's only one environment ever in play, so we should just
assume that
> > environment-
> >
> > That'll work for now, at least.
> It's not: we also check the tag value looks like a UUID. We'll be fine
once
> Launchpad is updated.
For now, though, LP is not updated. Is it not better to accept both
forms (ie in IsEnvironment too), treat any value as "this environment",
and target a bug for some future point at which we can drop the old
form?
https:/
File state/state_test.go (right):
https:/
state/state_
On 2013/11/28 09:42:48, axw wrote:
> On 2013/11/21 13:04:18, fwereade wrote:
> > We'll need explicit testing for the possibility of new services
racing with
> > dying environments. If you do a cleanup job for services, the races
won't be a
> > problem in themselves.
> I've added the cleanup, and added a test in cleanup_test.go that
ensures that a
> cleanup is scheduled to destroy preexisting services.
The cleanup is good -- it shows that any services that did exist before
the env was destroyed will be cleaned up -- but I think we still need to
prevent new services from being created, lest they be added after the
cleanup has run.
https:/
File state/environ.go (right):
https:/
state/environ.
I don't think this is quite right -- surely it's always ok to destroy an
environment and take down all the services, and all those other
machines? The real problem is manually-
kind, which need more carefu...
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
https:/
File state/environ.go (right):
https:/
state/environ.
On 2013/12/02 08:58:18, fwereade wrote:
> On 2013/11/28 09:42:48, axw wrote:
> > On 2013/11/21 13:04:18, fwereade wrote:
> > > If it's already dying we should abort, too, so this is just the
> assertAliveOp
> >
> > Why should we abort if Dying? While destroying other entities aborts
if
> they're
> > dying, they ultimately do not return an error to the caller.
> >
> > Is there a reason to prefer aborting and then checking if
Life==Dying and
> > returning nil? Is the no-op modification not really a no-op?
> >
> > I don't have a problem doing it, but I'd rather understand why
first.
> It's just defensive -- if the transaction ever grows such that it's
not a no-op,
> it'll already be correct.
Fair enough, thanks for explaining. Done.
https:/
File state/state_test.go (right):
https:/
state/state_
On 2013/12/02 08:58:18, fwereade wrote:
> On 2013/11/28 09:42:48, axw wrote:
> > On 2013/11/21 13:04:18, fwereade wrote:
> > > We'll need explicit testing for the possibility of new services
racing with
> > > dying environments. If you do a cleanup job for services, the
races won't be
> a
> > > problem in themselves.
> >
> > I've added the cleanup, and added a test in cleanup_test.go that
ensures that
> a
> > cleanup is scheduled to destroy preexisting services.
> The cleanup is good -- it shows that any services that did exist
before the env
> was destroyed will be cleaned up -- but I think we still need to
prevent new
> services from being created, lest they be added after the cleanup has
run.
This is handled with the "env.assertAliveOp" assertion in add-service
(and add-machine) transactions. Am I missing something?
https:/
File state/environ.go (right):
https:/
state/environ.
On 2013/12/02 08:58:18, fwereade wrote:
> I don't think this is quite right -- surely it's always ok to destroy
an
> environment and take down all the services, and all those other
machines? The
> real problem is manually-
need more
> careful cleanup (ie units and containers need to be removed properly).
After discussion on IRC, I'm going to remove this comment. I think the
way we're doing it is right. Destroy environment prevents new machines
and services; schedule cleanups for services to destroy the existing
ones; machines are destroyed via provider interface after
destroy-environment API returns successfully.
Manual is the exception, but for that we can just prevent
destroy-environment from completing while there are manual non-manager
machines in state, as discussed previously. We could, potentially,
destroy them later using the same mechanism as I intend to use for
manager machin...
Andrew Wilkins (axwalk) wrote : | # |
https:/
File state/state_test.go (right):
https:/
state/state_
On 2013/12/04 05:28:49, axw wrote:
> On 2013/12/02 08:58:18, fwereade wrote:
> > On 2013/11/28 09:42:48, axw wrote:
> > > On 2013/11/21 13:04:18, fwereade wrote:
> > > > We'll need explicit testing for the possibility of new services
racing
> with
> > > > dying environments. If you do a cleanup job for services, the
races won't
> be
> > a
> > > > problem in themselves.
> > >
> > > I've added the cleanup, and added a test in cleanup_test.go that
ensures
> that
> > a
> > > cleanup is scheduled to destroy preexisting services.
> >
> > The cleanup is good -- it shows that any services that did exist
before the
> env
> > was destroyed will be cleaned up -- but I think we still need to
prevent new
> > services from being created, lest they be added after the cleanup
has run.
> This is handled with the "env.assertAliveOp" assertion in add-service
(and
> add-machine) transactions. Am I missing something?
Ignore this comment, this was pre-discussion and I forgot to delete it.
William Reade (fwereade) wrote : | # |
Last few trivials. Thanks for bearing with me on this.
https:/
File state/cleanup.go (right):
https:/
state/cleanup.
Hmm, I do worry a bit about annotations. I guess it's probably not that
important here.
https:/
File state/environ.go (right):
https:/
state/environ.
FindId(
somewhere.
https:/
state/environ.
!= Alive?
https:/
state/environ.
destroy-
This isn't enforced in state, just at the API level; maybe make that
clearer?
Hopefully soon we'll have this stuff behind the API and be able to
maintain it properly...
https:/
state/environ.
Please, no panics in the API server. And it's not an error to destroy
something that's already dead -- given that this is currently the only
fail case, I think we're ok just returning nil.
If you're worried about the inconsistency compared to what I said about
the transaction list I wouldn't object to an explicit check -- in the
case where we can't figure out what went wrong we usually go with
ErrExcessiveCon
ErrSomeoneMesse
https:/
File state/initializ
https:/
state/initializ
s.State.
Shouldn't this still work, until we know no clients are still using it?
https:/
File state/state.go (right):
https:/
state/state.go:785: if err := env.Refresh(); err != nil {
Generally we go with "X is no longer alive", IIRC -- because then you
get a consistent message for a NotFound. Or at least it's *usually*
consistent, and in this case I think it always is, because to have made
a state connection implies the existence of this environment at some
point in the past.
Probably worth using that message for all the "is being destroyed"s in
this CL...
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
https:/
File state/cleanup.go (right):
https:/
state/cleanup.
On 2013/12/04 14:03:23, fwereade wrote:
> Hmm, I do worry a bit about annotations. I guess it's probably not
that
> important here.
Sorry, what about annotations?
https:/
File state/environ.go (right):
https:/
state/environ.
On 2013/12/04 14:03:23, fwereade wrote:
> FindId(
somewhere.
Updated to use FindId(UUID). State.Environment() still needs to use
Find(nil), since it doesn't know the UUID.
https:/
state/environ.
On 2013/12/04 14:03:23, fwereade wrote:
> != Alive?
Done.
https:/
state/environ.
destroy-
On 2013/12/04 14:03:23, fwereade wrote:
> This isn't enforced in state, just at the API level; maybe make that
clearer?
> Hopefully soon we'll have this stuff behind the API and be able to
maintain it
> properly...
Done.
https:/
state/environ.
On 2013/12/04 14:03:23, fwereade wrote:
> Please, no panics in the API server. And it's not an error to destroy
something
> that's already dead -- given that this is currently the only fail
case, I think
> we're ok just returning nil.
> If you're worried about the inconsistency compared to what I said
about the
> transaction list I wouldn't object to an explicit check -- in the case
where we
> can't figure out what went wrong we usually go with
ErrExcessiveCon
> though in practice it *usually* means ErrSomeoneMesse
Fixed. Sorry, I was under the mistaken impression that the other Destroy
methods returned an error if Destroy was called on something that's
dead. I appear to be mistaken.
https:/
File state/initializ
https:/
state/initializ
s.State.
On 2013/12/04 14:03:23, fwereade wrote:
> Shouldn't this still work, until we know no clients are still using
it?
Yes, it should and does. Backwards compatibility is tested explicitly in
TestFindEntity.
https:/
File state/state.go (right):
https:/
state/state.go:785: if err := env.Refresh(); err != nil {
On 2013/12/04 14:03:23, fwereade wrote:
> Generally we go with "X is no longer alive", IIRC -- because then you
get a
> consistent message for a NotFound. Or at least it's *usually*
c...
William Reade (fwereade) wrote : | # |
Final round of trivials. LGTM with the below if they're clear to you.
https:/
File state/cleanup.go (right):
https:/
state/cleanup.
On 2013/12/06 08:37:40, axw wrote:
> On 2013/12/04 14:03:23, fwereade wrote:
> > Hmm, I do worry a bit about annotations. I guess it's probably not
that
> > important here.
> Sorry, what about annotations?
The service type -- and a few others -- have annotator fields that we're
really lax about cloning properly because they were added really late
and aren't generally used (except by the API) IIRC. It doesn't matter...
until it does... but I think this is a case where it doesn't because
they won't leak out of this method at all.
It just tripped one of my "watch out for this" flags, but not usefully
or actionably -- I probably just shouldn't have mentioned it.
https:/
File state/environ.go (right):
https:/
state/environ.
On 2013/12/06 08:37:40, axw wrote:
> On 2013/12/04 14:03:23, fwereade wrote:
> > Please, no panics in the API server. And it's not an error to
destroy
> something
> > that's already dead -- given that this is currently the only fail
case, I
> think
> > we're ok just returning nil.
> >
> > If you're worried about the inconsistency compared to what I said
about the
> > transaction list I wouldn't object to an explicit check -- in the
case where
> we
> > can't figure out what went wrong we usually go with
ErrExcessiveCon
> (even
> > though in practice it *usually* means ErrSomeoneMesse
> Fixed. Sorry, I was under the mistaken impression that the other
Destroy methods
> returned an error if Destroy was called on something that's dead. I
appear to be
> mistaken.
No worries :). The rationale is that racing destroys are not a problem
-- both clients want it dead, and both clients' desires will be
satisfied no matter what order things happen in, so we should be good.
https:/
File state/environ.go (right):
https:/
state/environ.
This is a race and deserves a bug. Doesn't need to be fixed today
though.
https:/
state/environ.
hmm, but if it's actually *removed* -- in the multi-tenant future, in
which it can be removed -- this'll be an error where it doesn't actually
need to be. I'd be fine with setting `e.doc.Life = Dying` (and returning
nil) in the case of either nil or ErrAborted.
(FWIW, the reason that's not crazy is that setting it to Dying doesn't
violate any expectations at the client end: all the client sees is that
the lifecycle advanced, as expected, and if it happens to then refresh
and find it gone that's still consistent with the model. We couldn't set
Dead speculatively, because it might actually jus...
Andrew Wilkins (axwalk) wrote : | # |
https:/
File state/environ.go (right):
https:/
state/environ.
On 2013/12/09 10:14:41, fwereade wrote:
> This is a race and deserves a bug. Doesn't need to be fixed today
though.
Can you please clarify? What is the race exactly? Sorry if I'm being
dense.
https:/
state/environ.
On 2013/12/09 10:14:41, fwereade wrote:
> hmm, but if it's actually *removed* -- in the multi-tenant future, in
which it
> can be removed -- this'll be an error where it doesn't actually need
to be. I'd
> be fine with setting `e.doc.Life = Dying` (and returning nil) in the
case of
> either nil or ErrAborted.
> (FWIW, the reason that's not crazy is that setting it to Dying doesn't
violate
> any expectations at the client end: all the client sees is that the
lifecycle
> advanced, as expected, and if it happens to then refresh and find it
gone that's
> still consistent with the model. We couldn't set Dead speculatively,
because it
> might actually just be Dying, and thereby run the risk of a later
refresh moving
> the life state back from Dead to Dying: *that* would be a violation of
client
> expectations.)
Sounds crazy at first blush, but I understand and it sounds reasonable.
Will do.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
https:/
File state/environ.go (right):
https:/
state/environ.
On 2013/12/09 13:27:05, axw wrote:
> On 2013/12/09 10:14:41, fwereade wrote:
> > hmm, but if it's actually *removed* -- in the multi-tenant future,
in which it
> > can be removed -- this'll be an error where it doesn't actually need
to be.
> I'd
> > be fine with setting `e.doc.Life = Dying` (and returning nil) in the
case of
> > either nil or ErrAborted.
> >
> > (FWIW, the reason that's not crazy is that setting it to Dying
doesn't violate
> > any expectations at the client end: all the client sees is that the
lifecycle
> > advanced, as expected, and if it happens to then refresh and find it
gone
> that's
> > still consistent with the model. We couldn't set Dead speculatively,
because
> it
> > might actually just be Dying, and thereby run the risk of a later
refresh
> moving
> > the life state back from Dead to Dying: *that* would be a violation
of client
> > expectations.)
> Sounds crazy at first blush, but I understand and it sounds
reasonable. Will do.
Done.
https:/
state/environ.
asserts
On 2013/12/09 10:14:41, fwereade wrote:
> I think "read-only" is slightly ambiguous and doesn't really add
value. YMMV,
> though, keep it if you feel strongly.
Done.
https:/
File state/initializ
https:/
state/initializ
for environment-tag; see lp:1257587
On 2013/12/09 10:14:41, fwereade wrote:
> There's a standard format for these:
> TODO(person) date bug#
> explanation
Done.
https:/
File state/state.go (right):
https:/
state/state.go:492: return nil, fmt.Errorf(
alive")
On 2013/12/09 10:14:41, fwereade wrote:
> Again, future-proofing: if we get a NotFound, convert it to "no longer
alive".
Sorry, I don't see what that would future proof against. What's wrong
with just returning not found when the environment is not found?
https:/
state/state.go:631: if id != conf.Name() {
On 2013/12/09 10:14:41, fwereade wrote:
> Hmm. Should we log a warning instead here, and just return the env
anyway? As we
> move towards names being aliases, we open up the possibility of
multiple clients
> having different names for the same environment. That may be a
paranoion too far
> though... I can't see a path that enables multiple names before we
switch to
> using UUIDs across the board.
Do we need to support that before we move to UUIDs? I previously had the
code supporting anything, but changed it to check name again. That's
what it was doing before. Can we leave future...
William Reade (fwereade) wrote : | # |
https:/
File state/environ.go (right):
https:/
state/environ.
On 2013/12/09 13:27:05, axw wrote:
> On 2013/12/09 10:14:41, fwereade wrote:
> > This is a race and deserves a bug. Doesn't need to be fixed today
though.
> Can you please clarify? What is the race exactly? Sorry if I'm being
dense.
Client 1 checks no manual machines
Client 2 adds a manual machine
Client 1 destroys environment
Bad Thing.
If you're still around when I come online tomorrow morning, can we chat
a little bit about making machines' providers explicit? At the moment
the best we have is the "manual:" prefix on instance ids, but I don't
think that's quite adequate -- it's merely *unlikely* that such an id
willcollide with a legitimate one from another provider.
Andrew Wilkins (axwalk) wrote : | # |
On 2013/12/11 07:46:22, fwereade wrote:
> https:/
> File state/environ.go (right):
https:/
> state/environ.
> On 2013/12/09 13:27:05, axw wrote:
> > On 2013/12/09 10:14:41, fwereade wrote:
> > > This is a race and deserves a bug. Doesn't need to be fixed today
though.
> >
> > Can you please clarify? What is the race exactly? Sorry if I'm being
dense.
> Client 1 checks no manual machines
> Client 2 adds a manual machine
> Client 1 destroys environment
> Bad Thing.
As discussed on IRC: the way I *had* intended to handle that was to
first set Environment to Dying, and then check for manual machines. But,
as you point out, this leaves the environment in an unusable state if it
does fail. I'll leave the race in there (with a TODO), and we can come
back to it with a way to transactionally ensure no manual machines have
been added since last check.
> If you're still around when I come online tomorrow morning, can we
chat a little
> bit about making machines' providers explicit? At the moment the best
we have is
> the "manual:" prefix on instance ids, but I don't think that's quite
adequate --
> it's merely *unlikely* that such an id willcollide with a legitimate
one from
> another provider.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
https:/
File state/state.go (right):
https:/
state/state.go:492: return nil, fmt.Errorf(
alive")
On 2013/12/11 07:35:20, axw wrote:
> On 2013/12/09 10:14:41, fwereade wrote:
> > Again, future-proofing: if we get a NotFound, convert it to "no
longer alive".
> Sorry, I don't see what that would future proof against. What's wrong
with just
> returning not found when the environment is not found?
Done.
https:/
state/state.go:631: if id != conf.Name() {
On 2013/12/11 07:35:20, axw wrote:
> On 2013/12/09 10:14:41, fwereade wrote:
> > Hmm. Should we log a warning instead here, and just return the env
anyway? As
> we
> > move towards names being aliases, we open up the possibility of
multiple
> clients
> > having different names for the same environment. That may be a
paranoion too
> far
> > though... I can't see a path that enables multiple names before we
switch to
> > using UUIDs across the board.
> Do we need to support that before we move to UUIDs? I previously had
the code
> supporting anything, but changed it to check name again. That's what
it was
> doing before. Can we leave future functionality to the future, or does
this need
> to be done now?
I'm not convinced either way, but not doing it could put us in a
difficult position later. Done.
https:/
state/state.go:795: return nil, fmt.Errorf(
alive")
On 2013/12/09 10:14:41, fwereade wrote:
> ditto NotFound handling
Done.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
Preview Diff
1 | === modified file 'names/environ.go' |
2 | --- names/environ.go 2013-09-13 14:48:13 +0000 |
3 | +++ names/environ.go 2013-12-11 09:15:40 +0000 |
4 | @@ -7,13 +7,15 @@ |
5 | "strings" |
6 | ) |
7 | |
8 | -// EnvironTag returns the tag of an environment with the given name. |
9 | -func EnvironTag(name string) string { |
10 | - return makeTag(EnvironTagKind, name) |
11 | +// EnvironTag returns the tag of an environment with the given environment UUID. |
12 | +func EnvironTag(uuid string) string { |
13 | + return makeTag(EnvironTagKind, uuid) |
14 | } |
15 | |
16 | -// IsEnvironment returns whether id is a valid environment id. |
17 | -// TODO(rog) stricter constraints |
18 | -func IsEnvironment(name string) bool { |
19 | - return !strings.Contains(name, "/") |
20 | +// IsEnvironment returns whether id is a valid environment UUID. |
21 | +func IsEnvironment(id string) bool { |
22 | + // TODO(axw) 2013-12-04 #1257587 |
23 | + // We should not accept environment tags that |
24 | + // do not look like UUIDs. |
25 | + return !strings.Contains(id, "/") |
26 | } |
27 | |
28 | === modified file 'state/addmachine.go' |
29 | --- state/addmachine.go 2013-12-03 18:09:32 +0000 |
30 | +++ state/addmachine.go 2013-12-11 09:15:40 +0000 |
31 | @@ -10,6 +10,7 @@ |
32 | "labix.org/v2/mgo/txn" |
33 | |
34 | "launchpad.net/juju-core/constraints" |
35 | + "launchpad.net/juju-core/errors" |
36 | "launchpad.net/juju-core/instance" |
37 | "launchpad.net/juju-core/state/api/params" |
38 | "launchpad.net/juju-core/utils" |
39 | @@ -176,7 +177,20 @@ |
40 | } |
41 | |
42 | func (st *State) addMachine(mdoc *machineDoc, ops []txn.Op) (*Machine, error) { |
43 | + env, err := st.Environment() |
44 | + if err != nil { |
45 | + return nil, err |
46 | + } else if env.Life() != Alive { |
47 | + return nil, fmt.Errorf("environment is no longer alive") |
48 | + } |
49 | + ops = append([]txn.Op{env.assertAliveOp()}, ops...) |
50 | if err := st.runTransaction(ops); err != nil { |
51 | + enverr := env.Refresh() |
52 | + if (enverr == nil && env.Life() != Alive) || errors.IsNotFoundError(enverr) { |
53 | + return nil, fmt.Errorf("environment is no longer alive") |
54 | + } else if enverr != nil { |
55 | + err = enverr |
56 | + } |
57 | return nil, err |
58 | } |
59 | m := newMachine(st, mdoc) |
60 | |
61 | === modified file 'state/cleanup.go' |
62 | --- state/cleanup.go 2013-12-10 09:28:57 +0000 |
63 | +++ state/cleanup.go 2013-12-11 09:15:40 +0000 |
64 | @@ -55,6 +55,8 @@ |
65 | err = st.cleanupSettings(doc.Prefix) |
66 | case "units": |
67 | err = st.cleanupUnits(doc.Prefix) |
68 | + case "services": |
69 | + err = st.cleanupServices() |
70 | case "machine": |
71 | err = st.cleanupMachine(doc.Prefix) |
72 | default: |
73 | @@ -94,6 +96,26 @@ |
74 | return nil |
75 | } |
76 | |
77 | +// cleanupServices sets all services to Dying, if they are not already Dying |
78 | +// or Dead. It's expected to be used when an environment is destroyed. |
79 | +func (st *State) cleanupServices() error { |
80 | + // This won't miss services, because a Dying environment cannot have |
81 | + // services added to it. But we do have to remove the services themselves |
82 | + // via individual transactions, because they could be in any state at all. |
83 | + service := &Service{st: st} |
84 | + sel := D{{"life", Alive}} |
85 | + iter := st.services.Find(sel).Iter() |
86 | + for iter.Next(&service.doc) { |
87 | + if err := service.Destroy(); err != nil { |
88 | + return err |
89 | + } |
90 | + } |
91 | + if err := iter.Err(); err != nil { |
92 | + return fmt.Errorf("cannot read service document: %v", err) |
93 | + } |
94 | + return nil |
95 | +} |
96 | + |
97 | // cleanupUnits sets all units with the given prefix to Dying, if they are not |
98 | // already Dying or Dead. It's expected to be used when a service is destroyed. |
99 | func (st *State) cleanupUnits(prefix string) error { |
100 | |
101 | === modified file 'state/cleanup_test.go' |
102 | --- state/cleanup_test.go 2013-12-10 09:28:57 +0000 |
103 | +++ state/cleanup_test.go 2013-12-11 09:15:40 +0000 |
104 | @@ -55,6 +55,48 @@ |
105 | c.Assert(err, jc.Satisfies, errors.IsNotFoundError) |
106 | } |
107 | |
108 | +func (s *CleanupSuite) TestCleanupEnvironmentServices(c *gc.C) { |
109 | + s.assertDoesNotNeedCleanup(c) |
110 | + |
111 | + // Create a service with some units. |
112 | + mysql, err := s.State.AddService("mysql", s.AddTestingCharm(c, "mysql")) |
113 | + c.Assert(err, gc.IsNil) |
114 | + units := make([]*state.Unit, 3) |
115 | + for i := range units { |
116 | + unit, err := mysql.AddUnit() |
117 | + c.Assert(err, gc.IsNil) |
118 | + units[i] = unit |
119 | + } |
120 | + s.assertDoesNotNeedCleanup(c) |
121 | + |
122 | + // Destroy the environment and check the service and units are |
123 | + // unaffected, but a cleanup for the service has been scheduled. |
124 | + env, err := s.State.Environment() |
125 | + c.Assert(err, gc.IsNil) |
126 | + err = env.Destroy() |
127 | + c.Assert(err, gc.IsNil) |
128 | + s.assertNeedsCleanup(c) |
129 | + s.assertCleanupRuns(c) |
130 | + err = mysql.Refresh() |
131 | + c.Assert(err, gc.IsNil) |
132 | + c.Assert(mysql.Life(), gc.Equals, state.Dying) |
133 | + for _, unit := range units { |
134 | + err = unit.Refresh() |
135 | + c.Assert(err, gc.IsNil) |
136 | + c.Assert(unit.Life(), gc.Equals, state.Alive) |
137 | + } |
138 | + |
139 | + // The first cleanup Destroys the service, which |
140 | + // schedules another cleanup to destroy the units. |
141 | + s.assertNeedsCleanup(c) |
142 | + s.assertCleanupRuns(c) |
143 | + for _, unit := range units { |
144 | + err = unit.Refresh() |
145 | + c.Assert(err, jc.Satisfies, errors.IsNotFoundError) |
146 | + } |
147 | + s.assertDoesNotNeedCleanup(c) |
148 | +} |
149 | + |
150 | func (s *CleanupSuite) TestCleanupRelationSettings(c *gc.C) { |
151 | s.assertDoesNotNeedCleanup(c) |
152 | |
153 | |
154 | === modified file 'state/environ.go' |
155 | --- state/environ.go 2013-11-15 13:02:24 +0000 |
156 | +++ state/environ.go 2013-12-11 09:15:40 +0000 |
157 | @@ -26,21 +26,15 @@ |
158 | type environmentDoc struct { |
159 | UUID string `bson:"_id"` |
160 | Name string |
161 | + Life Life |
162 | } |
163 | |
164 | // Environment returns the environment entity. |
165 | func (st *State) Environment() (*Environment, error) { |
166 | - doc := environmentDoc{} |
167 | - err := st.environments.Find(nil).One(&doc) |
168 | - if err == mgo.ErrNotFound { |
169 | - return nil, errors.NotFoundf("environment") |
170 | - } else if err != nil { |
171 | + env := &Environment{st: st} |
172 | + if err := env.refresh(st.environments.Find(nil)); err != nil { |
173 | return nil, err |
174 | } |
175 | - env := &Environment{ |
176 | - st: st, |
177 | - doc: doc, |
178 | - } |
179 | env.annotator = annotator{ |
180 | globalKey: env.globalKey(), |
181 | tag: env.Tag(), |
182 | @@ -52,24 +46,77 @@ |
183 | // Tag returns a name identifying the environment. |
184 | // The returned name will be different from other Tag values returned |
185 | // by any other entities from the same state. |
186 | -func (e Environment) Tag() string { |
187 | - return names.EnvironTag(e.doc.Name) |
188 | +func (e *Environment) Tag() string { |
189 | + return names.EnvironTag(e.doc.UUID) |
190 | } |
191 | |
192 | // UUID returns the universally unique identifier of the environment. |
193 | -func (e Environment) UUID() string { |
194 | +func (e *Environment) UUID() string { |
195 | return e.doc.UUID |
196 | } |
197 | |
198 | +// Life returns whether the environment is Alive, Dying or Dead. |
199 | +func (e *Environment) Life() Life { |
200 | + return e.doc.Life |
201 | +} |
202 | + |
203 | // globalKey returns the global database key for the environment. |
204 | func (e *Environment) globalKey() string { |
205 | return environGlobalKey |
206 | } |
207 | |
208 | +func (e *Environment) Refresh() error { |
209 | + return e.refresh(e.st.environments.FindId(e.UUID())) |
210 | +} |
211 | + |
212 | +func (e *Environment) refresh(query *mgo.Query) error { |
213 | + err := query.One(&e.doc) |
214 | + if err == mgo.ErrNotFound { |
215 | + return errors.NotFoundf("environment") |
216 | + } |
217 | + return err |
218 | +} |
219 | + |
220 | +// Destroy sets the environment's lifecycle to Dying, preventing |
221 | +// addition of services or machines to state. |
222 | +func (e *Environment) Destroy() error { |
223 | + if e.Life() != Alive { |
224 | + return nil |
225 | + } |
226 | + // TODO(axw) 2013-12-11 #1218688 |
227 | + // Resolve the race between checking for manual machines and |
228 | + // destroying the environment. We can set Environment to Dying |
229 | + // to resolve the race, but that puts the environment into an |
230 | + // unusable state. |
231 | + // |
232 | + // We add a cleanup for services, but not for machines; |
233 | + // machines are destroyed via the provider interface. The |
234 | + // exception to this rule is manual machines; the API prevents |
235 | + // destroy-environment from succeeding if any non-manager |
236 | + // manual machines exist. |
237 | + ops := []txn.Op{{ |
238 | + C: e.st.environments.Name, |
239 | + Id: e.doc.UUID, |
240 | + Update: D{{"$set", D{{"life", Dying}}}}, |
241 | + Assert: isAliveDoc, |
242 | + }, e.st.newCleanupOp("services", "")} |
243 | + err := e.st.runTransaction(ops) |
244 | + switch err { |
245 | + case nil, txn.ErrAborted: |
246 | + // If the transaction aborted, the environment is either |
247 | + // Dying or Dead; neither case is an error. If it's Dead, |
248 | + // reporting it as Dying is not incorrect; the user thought |
249 | + // it was Alive, so we've progressed towards Dead. If the |
250 | + // user then calls Refresh they'll get the true value. |
251 | + e.doc.Life = Dying |
252 | + } |
253 | + return err |
254 | +} |
255 | + |
256 | // createEnvironmentOp returns the operation needed to create |
257 | // an environment document with the given name and UUID. |
258 | func createEnvironmentOp(st *State, name, uuid string) txn.Op { |
259 | - doc := &environmentDoc{uuid, name} |
260 | + doc := &environmentDoc{uuid, name, Alive} |
261 | return txn.Op{ |
262 | C: st.environments.Name, |
263 | Id: uuid, |
264 | @@ -77,3 +124,12 @@ |
265 | Insert: doc, |
266 | } |
267 | } |
268 | + |
269 | +// assertAliveOp returns a txn.Op that asserts the environment is alive. |
270 | +func (e *Environment) assertAliveOp() txn.Op { |
271 | + return txn.Op{ |
272 | + C: e.st.environments.Name, |
273 | + Id: e.UUID(), |
274 | + Assert: isAliveDoc, |
275 | + } |
276 | +} |
277 | |
278 | === modified file 'state/environ_test.go' |
279 | --- state/environ_test.go 2013-08-19 11:20:02 +0000 |
280 | +++ state/environ_test.go 2013-12-11 09:15:40 +0000 |
281 | @@ -24,9 +24,7 @@ |
282 | } |
283 | |
284 | func (s *EnvironSuite) TestTag(c *gc.C) { |
285 | - cfg, err := s.State.EnvironConfig() |
286 | - c.Assert(err, gc.IsNil) |
287 | - expected := "environment-" + cfg.Name() |
288 | + expected := "environment-" + s.env.UUID() |
289 | c.Assert(s.env.Tag(), gc.Equals, expected) |
290 | } |
291 | |
292 | |
293 | === modified file 'state/initialize_test.go' |
294 | --- state/initialize_test.go 2013-12-06 04:47:45 +0000 |
295 | +++ state/initialize_test.go 2013-12-11 09:15:40 +0000 |
296 | @@ -51,6 +51,8 @@ |
297 | _, err := s.State.EnvironConfig() |
298 | c.Assert(err, jc.Satisfies, errors.IsNotFoundError) |
299 | _, err = s.State.FindEntity("environment-foo") |
300 | + // TODO(axw) 2013-12-04 #1257587 |
301 | + // remove backwards compatibility for environment-tag; see state.go |
302 | c.Assert(err, jc.Satisfies, errors.IsNotFoundError) |
303 | _, err = s.State.EnvironConstraints() |
304 | c.Assert(err, jc.Satisfies, errors.IsNotFoundError) |
305 | @@ -66,10 +68,13 @@ |
306 | cfg, err = s.State.EnvironConfig() |
307 | c.Assert(err, gc.IsNil) |
308 | c.Assert(cfg.AllAttrs(), gc.DeepEquals, initial) |
309 | - env0, err := s.State.FindEntity("environment-" + cfg.Name()) |
310 | - c.Assert(err, gc.IsNil) |
311 | - env := env0.(state.Annotator) |
312 | - annotations, err := env.Annotations() |
313 | + |
314 | + env, err := s.State.Environment() |
315 | + c.Assert(err, gc.IsNil) |
316 | + entity, err := s.State.FindEntity("environment-" + env.UUID()) |
317 | + c.Assert(err, gc.IsNil) |
318 | + annotator := entity.(state.Annotator) |
319 | + annotations, err := annotator.Annotations() |
320 | c.Assert(err, gc.IsNil) |
321 | c.Assert(annotations, gc.HasLen, 0) |
322 | cons, err := s.State.EnvironConstraints() |
323 | |
324 | === modified file 'state/interface.go' |
325 | --- state/interface.go 2013-10-02 13:14:49 +0000 |
326 | +++ state/interface.go 2013-12-11 09:15:40 +0000 |
327 | @@ -128,6 +128,7 @@ |
328 | _ NotifyWatcherFactory = (*Machine)(nil) |
329 | _ NotifyWatcherFactory = (*Unit)(nil) |
330 | _ NotifyWatcherFactory = (*Service)(nil) |
331 | + _ NotifyWatcherFactory = (*Environment)(nil) |
332 | ) |
333 | |
334 | // AgentEntity represents an entity that can |
335 | |
336 | === modified file 'state/state.go' |
337 | --- state/state.go 2013-12-06 04:47:45 +0000 |
338 | +++ state/state.go 2013-12-11 09:15:40 +0000 |
339 | @@ -390,16 +390,29 @@ |
340 | case names.ServiceTagKind: |
341 | return st.Service(id) |
342 | case names.EnvironTagKind: |
343 | - conf, err := st.EnvironConfig() |
344 | + env, err := st.Environment() |
345 | if err != nil { |
346 | return nil, err |
347 | } |
348 | // Return an invalid entity error if the requested environment is not |
349 | // the current one. |
350 | - if id != conf.Name() { |
351 | - return nil, errors.NotFoundf("environment %q", id) |
352 | + if id != env.UUID() { |
353 | + if utils.IsValidUUIDString(id) { |
354 | + return nil, errors.NotFoundf("environment %q", id) |
355 | + } |
356 | + // TODO(axw) 2013-12-04 #1257587 |
357 | + // We should not accept environment tags that do not match the |
358 | + // environment's UUID. We accept anything for now, to cater |
359 | + // both for past usage, and for potentially supporting aliases. |
360 | + logger.Warningf("environment-tag does not match current environment UUID: %q != %q", id, env.UUID()) |
361 | + conf, err := st.EnvironConfig() |
362 | + if err != nil { |
363 | + logger.Warningf("EnvironConfig failed: %v", err) |
364 | + } else if id != conf.Name() { |
365 | + logger.Warningf("environment-tag does not match current environment name: %q != %q", id, conf.Name()) |
366 | + } |
367 | } |
368 | - return st.Environment() |
369 | + return env, nil |
370 | case names.RelationTagKind: |
371 | return st.KeyRelation(id) |
372 | } |
373 | @@ -424,6 +437,8 @@ |
374 | coll = st.users.Name |
375 | case names.RelationTagKind: |
376 | coll = st.relations.Name |
377 | + case names.EnvironTagKind: |
378 | + coll = st.environments.Name |
379 | default: |
380 | return "", "", fmt.Errorf("%q is not a valid collection tag", tag) |
381 | } |
382 | @@ -511,6 +526,12 @@ |
383 | } else if exists { |
384 | return nil, fmt.Errorf("service already exists") |
385 | } |
386 | + env, err := st.Environment() |
387 | + if err != nil { |
388 | + return nil, err |
389 | + } else if env.Life() != Alive { |
390 | + return nil, fmt.Errorf("environment is no longer alive") |
391 | + } |
392 | // Create the service addition operations. |
393 | peers := ch.Meta().Peers |
394 | svcDoc := &serviceDoc{ |
395 | @@ -523,6 +544,7 @@ |
396 | } |
397 | svc := newService(st, svcDoc) |
398 | ops := []txn.Op{ |
399 | + env.assertAliveOp(), |
400 | createConstraintsOp(st, svc.globalKey(), constraints.Value{}), |
401 | createSettingsOp(st, svc.settingsKey(), nil), |
402 | { |
403 | @@ -544,9 +566,15 @@ |
404 | ops = append(ops, peerOps...) |
405 | |
406 | // Run the transaction; happily, there's never any reason to retry, |
407 | - // because all the possible failed assertions imply that the service |
408 | - // already exists. |
409 | + // because all the possible failed assertions imply that either the |
410 | + // service already exists, or the environment is being destroyed. |
411 | if err := st.runTransaction(ops); err == txn.ErrAborted { |
412 | + err := env.Refresh() |
413 | + if (err == nil && env.Life() != Alive) || errors.IsNotFoundError(err) { |
414 | + return nil, fmt.Errorf("environment is no longer alive") |
415 | + } else if err != nil { |
416 | + return nil, err |
417 | + } |
418 | return nil, fmt.Errorf("service already exists") |
419 | } else if err != nil { |
420 | return nil, err |
421 | |
422 | === modified file 'state/state_test.go' |
423 | --- state/state_test.go 2013-12-06 04:47:45 +0000 |
424 | +++ state/state_test.go 2013-12-11 09:15:40 +0000 |
425 | @@ -207,6 +207,33 @@ |
426 | check(m[1], "1", "blahblah", allJobs) |
427 | } |
428 | |
429 | +func (s *StateSuite) TestAddMachinesEnvironmentDying(c *gc.C) { |
430 | + _, err := s.State.AddMachine("quantal", state.JobHostUnits) |
431 | + c.Assert(err, gc.IsNil) |
432 | + env, err := s.State.Environment() |
433 | + c.Assert(err, gc.IsNil) |
434 | + err = env.Destroy() |
435 | + c.Assert(err, gc.IsNil) |
436 | + // Check that machines cannot be added if the environment is initially Dying. |
437 | + _, err = s.State.AddMachine("quantal", state.JobHostUnits) |
438 | + c.Assert(err, gc.ErrorMatches, "cannot add a new machine: environment is no longer alive") |
439 | +} |
440 | + |
441 | +func (s *StateSuite) TestAddMachinesEnvironmentDyingAfterInitial(c *gc.C) { |
442 | + _, err := s.State.AddMachine("quantal", state.JobHostUnits) |
443 | + c.Assert(err, gc.IsNil) |
444 | + env, err := s.State.Environment() |
445 | + c.Assert(err, gc.IsNil) |
446 | + // Check that machines cannot be added if the environment is initially |
447 | + // Alive but set to Dying immediately before the transaction is run. |
448 | + defer state.SetBeforeHooks(c, s.State, func() { |
449 | + c.Assert(env.Life(), gc.Equals, state.Alive) |
450 | + c.Assert(env.Destroy(), gc.IsNil) |
451 | + }).Check() |
452 | + _, err = s.State.AddMachine("quantal", state.JobHostUnits) |
453 | + c.Assert(err, gc.ErrorMatches, "cannot add a new machine: environment is no longer alive") |
454 | +} |
455 | + |
456 | func (s *StateSuite) TestAddMachineExtraConstraints(c *gc.C) { |
457 | err := s.State.SetEnvironConstraints(constraints.MustParse("mem=4G")) |
458 | c.Assert(err, gc.IsNil) |
459 | @@ -585,6 +612,35 @@ |
460 | c.Assert(ch.URL(), gc.DeepEquals, charm.URL()) |
461 | } |
462 | |
463 | +func (s *StateSuite) TestAddServiceEnvironmentDying(c *gc.C) { |
464 | + charm := s.AddTestingCharm(c, "dummy") |
465 | + _, err := s.State.AddService("s0", charm) |
466 | + c.Assert(err, gc.IsNil) |
467 | + // Check that services cannot be added if the environment is initially Dying. |
468 | + env, err := s.State.Environment() |
469 | + c.Assert(err, gc.IsNil) |
470 | + err = env.Destroy() |
471 | + c.Assert(err, gc.IsNil) |
472 | + _, err = s.State.AddService("s1", charm) |
473 | + c.Assert(err, gc.ErrorMatches, `cannot add service "s1": environment is no longer alive`) |
474 | +} |
475 | + |
476 | +func (s *StateSuite) TestAddServiceEnvironmentDyingAfterInitial(c *gc.C) { |
477 | + charm := s.AddTestingCharm(c, "dummy") |
478 | + _, err := s.State.AddService("s0", charm) |
479 | + c.Assert(err, gc.IsNil) |
480 | + env, err := s.State.Environment() |
481 | + c.Assert(err, gc.IsNil) |
482 | + // Check that services cannot be added if the environment is initially |
483 | + // Alive but set to Dying immediately before the transaction is run. |
484 | + defer state.SetBeforeHooks(c, s.State, func() { |
485 | + c.Assert(env.Life(), gc.Equals, state.Alive) |
486 | + c.Assert(env.Destroy(), gc.IsNil) |
487 | + }).Check() |
488 | + _, err = s.State.AddService("s1", charm) |
489 | + c.Assert(err, gc.ErrorMatches, `cannot add service "s1": environment is no longer alive`) |
490 | +} |
491 | + |
492 | func (s *StateSuite) TestServiceNotFound(c *gc.C) { |
493 | _, err := s.State.Service("bummer") |
494 | c.Assert(err, gc.ErrorMatches, `service "bummer" not found`) |
495 | @@ -1611,10 +1667,12 @@ |
496 | c.Assert(err, gc.IsNil) |
497 | } |
498 | |
499 | -var findEntityTests = []struct { |
500 | +type findEntityTest struct { |
501 | tag string |
502 | err string |
503 | -}{{ |
504 | +} |
505 | + |
506 | +var findEntityTests = []findEntityTest{{ |
507 | tag: "", |
508 | err: `"" is not a valid tag`, |
509 | }, { |
510 | @@ -1651,8 +1709,8 @@ |
511 | tag: "service-foo/bar", |
512 | err: `"service-foo/bar" is not a valid service tag`, |
513 | }, { |
514 | - tag: "environment-foo", |
515 | - err: `environment "foo" not found`, |
516 | + tag: "environment-9f484882-2f18-4fd2-967d-db9663db7bea", |
517 | + err: `environment "9f484882-2f18-4fd2-967d-db9663db7bea" not found`, |
518 | }, { |
519 | tag: "machine-1234", |
520 | err: `machine 1234 not found`, |
521 | @@ -1673,7 +1731,13 @@ |
522 | }, { |
523 | tag: "user-arble", |
524 | }, { |
525 | + // TODO(axw) 2013-12-04 #1257587 |
526 | + // remove backwards compatibility for environment-tag; see state.go |
527 | + tag: "environment-notauuid", |
528 | + //err: `"environment-notauuid" is not a valid environment tag`, |
529 | +}, { |
530 | tag: "environment-testenv", |
531 | + //err: `"environment-testenv" is not a valid environment tag`, |
532 | }} |
533 | |
534 | var entityTypes = map[string]interface{}{ |
535 | @@ -1702,6 +1766,14 @@ |
536 | c.Assert(err, gc.IsNil) |
537 | c.Assert(rel.String(), gc.Equals, "wordpress:db ser-vice2:server") |
538 | |
539 | + // environment tag is dynamically generated |
540 | + env, err := s.State.Environment() |
541 | + c.Assert(err, gc.IsNil) |
542 | + findEntityTests = append([]findEntityTest{}, findEntityTests...) |
543 | + findEntityTests = append(findEntityTests, findEntityTest{ |
544 | + tag: "environment-" + env.UUID(), |
545 | + }) |
546 | + |
547 | for i, test := range findEntityTests { |
548 | c.Logf("test %d: %q", i, test.tag) |
549 | e, err := s.State.FindEntity(test.tag) |
550 | @@ -1712,7 +1784,14 @@ |
551 | kind, err := names.TagKind(test.tag) |
552 | c.Assert(err, gc.IsNil) |
553 | c.Assert(e, gc.FitsTypeOf, entityTypes[kind]) |
554 | - c.Assert(e.Tag(), gc.Equals, test.tag) |
555 | + if kind == "environment" { |
556 | + // TODO(axw) 2013-12-04 #1257587 |
557 | + // We *should* only be able to get the entity with its tag, but |
558 | + // for backwards-compatibility we accept any non-UUID tag. |
559 | + c.Assert(e.Tag(), gc.Equals, env.Tag()) |
560 | + } else { |
561 | + c.Assert(e.Tag(), gc.Equals, test.tag) |
562 | + } |
563 | } |
564 | } |
565 | } |
566 | @@ -1725,7 +1804,6 @@ |
567 | "foo-", |
568 | "---", |
569 | "foo-bar", |
570 | - "environment-foo", |
571 | "unit-foo", |
572 | } |
573 | for _, name := range bad { |
574 | @@ -1767,6 +1845,14 @@ |
575 | c.Assert(coll, gc.Equals, "users") |
576 | c.Assert(id, gc.Equals, user.Name()) |
577 | c.Assert(err, gc.IsNil) |
578 | + |
579 | + // Parse an environment entity name. |
580 | + env, err := s.State.Environment() |
581 | + c.Assert(err, gc.IsNil) |
582 | + coll, id, err = state.ParseTag(s.State, env.Tag()) |
583 | + c.Assert(coll, gc.Equals, "environments") |
584 | + c.Assert(id, gc.Equals, env.UUID()) |
585 | + c.Assert(err, gc.IsNil) |
586 | } |
587 | |
588 | func (s *StateSuite) TestWatchCleanups(c *gc.C) { |
589 | |
590 | === modified file 'state/watcher.go' |
591 | --- state/watcher.go 2013-11-25 05:03:44 +0000 |
592 | +++ state/watcher.go 2013-12-11 09:15:40 +0000 |
593 | @@ -1070,6 +1070,11 @@ |
594 | return newEntityWatcher(u.st, u.st.units, u.doc.Name) |
595 | } |
596 | |
597 | +// Watch returns a watcher for observing changes to an environment. |
598 | +func (e *Environment) Watch() NotifyWatcher { |
599 | + return newEntityWatcher(e.st, e.st.environments, e.doc.UUID) |
600 | +} |
601 | + |
602 | // WatchForEnvironConfigChanges return a NotifyWatcher waiting for the Environ |
603 | // Config to change. This differs from WatchEnvironConfig in that the watcher |
604 | // is a NotifyWatcher that does not give content during Changes() |
Reviewers: mp+195710_ code.launchpad. net,
Message:
Please take a look.
Description:
state: add Life to state.Environment
Environment has been given new methods
to mark an environment as dying, and
to remove the environment from state.
An environment must now exist in state
and be Alive for services or machines
to be added to state.
A secondary change here is to use the
environment's UUID in the tag, rather
than the environment's name, which is
non-unique.
https:/ /code.launchpad .net/~axwalk/ juju-core/ state-environme nt-life/ +merge/ 195710
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/28880043/
Affected files (+172, -34 lines): test.go e_test. go
A [revision details]
M names/environ.go
M state/environ.go
M state/environ_
M state/initializ
M state/interface.go
M state/state.go
M state/state_test.go
M state/watcher.go