Merge lp:~rogpeppe/pyjuju/go-state-units-under-service into lp:pyjuju/go

Proposed by Roger Peppe
Status: Rejected
Rejected by: Roger Peppe
Proposed branch: lp:~rogpeppe/pyjuju/go-state-units-under-service
Merge into: lp:pyjuju/go
Diff against target: 20549 lines (+19973/-0) (has conflicts)
111 files modified
.lbox.OTHER (+1/-0)
charm/bundle_test.go.OTHER (+196/-0)
charm/charm_test.go.OTHER (+97/-0)
charm/config.go.OTHER (+141/-0)
charm/config_test.go.OTHER (+191/-0)
charm/dir_test.go.OTHER (+199/-0)
charm/export_test.go.OTHER (+15/-0)
charm/meta.go.OTHER (+201/-0)
charm/meta_test.go.OTHER (+137/-0)
charm/repo.go.OTHER (+226/-0)
charm/repo_test.go.OTHER (+339/-0)
charm/url_test.go.OTHER (+126/-0)
cloudinit/cloudinit_test.go.OTHER (+228/-0)
cmd/cmd.go.OTHER (+139/-0)
cmd/cmd_test.go.OTHER (+96/-0)
cmd/juju/bootstrap.go.OTHER (+37/-0)
cmd/juju/cmd_test.go.OTHER (+255/-0)
cmd/juju/deploy.go.OTHER (+79/-0)
cmd/juju/destroy.go.OTHER (+36/-0)
cmd/juju/main.go.OTHER (+40/-0)
cmd/jujuc/main.go.OTHER (+89/-0)
cmd/jujuc/main_test.go.OTHER (+159/-0)
cmd/jujuc/server/config-get.go.OTHER (+66/-0)
cmd/jujuc/server/config-get_test.go.OTHER (+125/-0)
cmd/jujuc/server/context.go.OTHER (+96/-0)
cmd/jujuc/server/context_test.go.OTHER (+145/-0)
cmd/jujuc/server/juju-log.go.OTHER (+57/-0)
cmd/jujuc/server/juju-log_test.go.OTHER (+82/-0)
cmd/jujuc/server/output.go.OTHER (+149/-0)
cmd/jujuc/server/ports.go.OTHER (+100/-0)
cmd/jujuc/server/ports_test.go.OTHER (+86/-0)
cmd/jujuc/server/server.go.OTHER (+133/-0)
cmd/jujuc/server/server_test.go.OTHER (+146/-0)
cmd/jujuc/server/unit-get.go.OTHER (+66/-0)
cmd/jujuc/server/unit-get_test.go.OTHER (+109/-0)
cmd/jujuc/server/util_test.go.OTHER (+149/-0)
cmd/jujud/agent.go.OTHER (+70/-0)
cmd/jujud/initzk.go.OTHER (+44/-0)
cmd/jujud/initzk_test.go.OTHER (+78/-0)
cmd/jujud/machine.go.OTHER (+36/-0)
cmd/jujud/machine_test.go.OTHER (+35/-0)
cmd/jujud/main.go.OTHER (+29/-0)
cmd/jujud/main_test.go.OTHER (+67/-0)
cmd/jujud/provisioning.go.OTHER (+142/-0)
cmd/jujud/provisioning_test.go.OTHER (+107/-0)
cmd/jujud/unit.go.OTHER (+40/-0)
cmd/jujud/unit_test.go.OTHER (+44/-0)
cmd/jujud/util_test.go.OTHER (+46/-0)
cmd/logging.go.OTHER (+46/-0)
cmd/logging_test.go.OTHER (+104/-0)
cmd/supercommand_test.go.OTHER (+96/-0)
cmd/util_test.go.OTHER (+78/-0)
environs/config_test.go.OTHER (+176/-0)
environs/dummy/environs.go.OTHER (+411/-0)
environs/dummy/environs_test.go.OTHER (+41/-0)
environs/dummy/storage.go.OTHER (+97/-0)
environs/ec2/auth_test.go.OTHER (+87/-0)
environs/ec2/cloudinit.go.OTHER (+165/-0)
environs/ec2/cloudinit_test.go.OTHER (+210/-0)
environs/ec2/config.go.OTHER (+98/-0)
environs/ec2/config_test.go.OTHER (+196/-0)
environs/ec2/ec2.go.OTHER (+634/-0)
environs/ec2/export_test.go.OTHER (+102/-0)
environs/ec2/image.go.OTHER (+111/-0)
environs/ec2/live_test.go.OTHER (+348/-0)
environs/ec2/local_test.go.OTHER (+254/-0)
environs/ec2/state.go.OTHER (+48/-0)
environs/ec2/storage.go.OTHER (+127/-0)
environs/ec2/suite_test.go.OTHER (+22/-0)
environs/interface.go.OTHER (+149/-0)
environs/jujutest/livetests.go.OTHER (+116/-0)
environs/jujutest/test.go.OTHER (+99/-0)
environs/jujutest/tests.go.OTHER (+179/-0)
environs/open_test.go.OTHER (+53/-0)
environs/tools.go.OTHER (+322/-0)
environs/tools_test.go.OTHER (+292/-0)
juju/conn.go.OTHER (+77/-0)
juju/conn_test.go.OTHER (+89/-0)
log/log_test.go.OTHER (+54/-0)
schema/schema_test.go.OTHER (+292/-0)
state/charm.go.OTHER (+88/-0)
state/export_test.go.OTHER (+40/-0)
state/internal_test.go.OTHER (+1109/-0)
state/machine.go.OTHER (+120/-0)
state/open.go.OTHER (+160/-0)
state/presence/presence_test.go.OTHER (+320/-0)
state/relation.go.OTHER (+86/-0)
state/service.go.OTHER (+268/-0)
state/ssh.go.OTHER (+282/-0)
state/ssh_test.go.OTHER (+494/-0)
state/state.go.OTHER (+393/-0)
state/state_test.go.OTHER (+1253/-0)
state/topology.go.OTHER (+587/-0)
state/unit.go.OTHER (+611/-0)
state/watcher.go.OTHER (+411/-0)
state/watcher/watcher_test.go.OTHER (+168/-0)
state/watcher_test.go.OTHER (+355/-0)
store/branch.go.OTHER (+146/-0)
store/branch_test.go.OTHER (+195/-0)
store/charmd/main.go.OTHER (+74/-0)
store/charmload/main.go.OTHER (+75/-0)
store/lpad.go.OTHER (+113/-0)
store/lpad_test.go.OTHER (+68/-0)
store/server.go.OTHER (+190/-0)
store/server_test.go.OTHER (+208/-0)
store/store.go.OTHER (+762/-0)
store/store_test.go.OTHER (+574/-0)
testing/charm.go.OTHER (+82/-0)
testing/log.go.OTHER (+25/-0)
testing/zk_test.go.OTHER (+58/-0)
upstart/upstart_test.go.OTHER (+211/-0)
Contents conflict in .lbox
Conflict adding files to charm.  Created directory.
Conflict because charm is not versioned, but has versioned children.  Versioned directory.
Contents conflict in charm/bundle_test.go
Contents conflict in charm/charm_test.go
Contents conflict in charm/config.go
Contents conflict in charm/config_test.go
Contents conflict in charm/dir_test.go
Contents conflict in charm/export_test.go
Contents conflict in charm/meta.go
Contents conflict in charm/meta_test.go
Contents conflict in charm/repo.go
Contents conflict in charm/repo_test.go
Contents conflict in charm/url_test.go
Conflict adding files to cloudinit.  Created directory.
Conflict because cloudinit is not versioned, but has versioned children.  Versioned directory.
Contents conflict in cloudinit/cloudinit_test.go
Conflict adding files to cmd.  Created directory.
Conflict because cmd is not versioned, but has versioned children.  Versioned directory.
Contents conflict in cmd/cmd.go
Contents conflict in cmd/cmd_test.go
Conflict adding files to cmd/juju.  Created directory.
Conflict because cmd/juju is not versioned, but has versioned children.  Versioned directory.
Contents conflict in cmd/juju/bootstrap.go
Contents conflict in cmd/juju/cmd_test.go
Contents conflict in cmd/juju/deploy.go
Contents conflict in cmd/juju/destroy.go
Contents conflict in cmd/juju/main.go
Conflict adding files to cmd/jujuc.  Created directory.
Conflict because cmd/jujuc is not versioned, but has versioned children.  Versioned directory.
Contents conflict in cmd/jujuc/main.go
Contents conflict in cmd/jujuc/main_test.go
Conflict adding files to cmd/jujuc/server.  Created directory.
Conflict because cmd/jujuc/server is not versioned, but has versioned children.  Versioned directory.
Contents conflict in cmd/jujuc/server/config-get.go
Contents conflict in cmd/jujuc/server/config-get_test.go
Contents conflict in cmd/jujuc/server/context.go
Contents conflict in cmd/jujuc/server/context_test.go
Contents conflict in cmd/jujuc/server/juju-log.go
Contents conflict in cmd/jujuc/server/juju-log_test.go
Contents conflict in cmd/jujuc/server/output.go
Contents conflict in cmd/jujuc/server/ports.go
Contents conflict in cmd/jujuc/server/ports_test.go
Contents conflict in cmd/jujuc/server/server.go
Contents conflict in cmd/jujuc/server/server_test.go
Contents conflict in cmd/jujuc/server/unit-get.go
Contents conflict in cmd/jujuc/server/unit-get_test.go
Contents conflict in cmd/jujuc/server/util_test.go
Conflict adding files to cmd/jujud.  Created directory.
Conflict because cmd/jujud is not versioned, but has versioned children.  Versioned directory.
Contents conflict in cmd/jujud/agent.go
Contents conflict in cmd/jujud/initzk.go
Contents conflict in cmd/jujud/initzk_test.go
Contents conflict in cmd/jujud/machine.go
Contents conflict in cmd/jujud/machine_test.go
Contents conflict in cmd/jujud/main.go
Contents conflict in cmd/jujud/main_test.go
Contents conflict in cmd/jujud/provisioning.go
Contents conflict in cmd/jujud/provisioning_test.go
Contents conflict in cmd/jujud/unit.go
Contents conflict in cmd/jujud/unit_test.go
Contents conflict in cmd/jujud/util_test.go
Contents conflict in cmd/logging.go
Contents conflict in cmd/logging_test.go
Contents conflict in cmd/supercommand_test.go
Contents conflict in cmd/util_test.go
Conflict adding files to environs.  Created directory.
Conflict because environs is not versioned, but has versioned children.  Versioned directory.
Contents conflict in environs/config_test.go
Conflict adding files to environs/dummy.  Created directory.
Conflict because environs/dummy is not versioned, but has versioned children.  Versioned directory.
Contents conflict in environs/dummy/environs.go
Contents conflict in environs/dummy/environs_test.go
Contents conflict in environs/dummy/storage.go
Conflict adding files to environs/ec2.  Created directory.
Conflict because environs/ec2 is not versioned, but has versioned children.  Versioned directory.
Contents conflict in environs/ec2/auth_test.go
Contents conflict in environs/ec2/cloudinit.go
Contents conflict in environs/ec2/cloudinit_test.go
Contents conflict in environs/ec2/config.go
Contents conflict in environs/ec2/config_test.go
Contents conflict in environs/ec2/ec2.go
Contents conflict in environs/ec2/export_test.go
Contents conflict in environs/ec2/image.go
Contents conflict in environs/ec2/live_test.go
Contents conflict in environs/ec2/local_test.go
Contents conflict in environs/ec2/state.go
Contents conflict in environs/ec2/storage.go
Contents conflict in environs/ec2/suite_test.go
Contents conflict in environs/interface.go
Conflict adding files to environs/jujutest.  Created directory.
Conflict because environs/jujutest is not versioned, but has versioned children.  Versioned directory.
Contents conflict in environs/jujutest/livetests.go
Contents conflict in environs/jujutest/test.go
Contents conflict in environs/jujutest/tests.go
Contents conflict in environs/open_test.go
Contents conflict in environs/tools.go
Contents conflict in environs/tools_test.go
Conflict adding files to juju.  Created directory.
Conflict because juju is not versioned, but has versioned children.  Versioned directory.
Contents conflict in juju/conn.go
Contents conflict in juju/conn_test.go
Conflict adding files to log.  Created directory.
Conflict because log is not versioned, but has versioned children.  Versioned directory.
Contents conflict in log/log_test.go
Conflict adding files to schema.  Created directory.
Conflict because schema is not versioned, but has versioned children.  Versioned directory.
Contents conflict in schema/schema_test.go
Conflict adding files to state.  Created directory.
Conflict because state is not versioned, but has versioned children.  Versioned directory.
Contents conflict in state/charm.go
Contents conflict in state/export_test.go
Contents conflict in state/internal_test.go
Contents conflict in state/machine.go
Contents conflict in state/open.go
Conflict adding files to state/presence.  Created directory.
Conflict because state/presence is not versioned, but has versioned children.  Versioned directory.
Contents conflict in state/presence/presence_test.go
Contents conflict in state/relation.go
Contents conflict in state/service.go
Contents conflict in state/ssh.go
Contents conflict in state/ssh_test.go
Contents conflict in state/state.go
Contents conflict in state/state_test.go
Contents conflict in state/topology.go
Contents conflict in state/unit.go
Conflict adding files to state/watcher.  Created directory.
Conflict because state/watcher is not versioned, but has versioned children.  Versioned directory.
Contents conflict in state/watcher.go
Contents conflict in state/watcher/watcher_test.go
Contents conflict in state/watcher_test.go
Conflict adding files to store.  Created directory.
Conflict because store is not versioned, but has versioned children.  Versioned directory.
Contents conflict in store/branch.go
Contents conflict in store/branch_test.go
Conflict adding files to store/charmd.  Created directory.
Conflict because store/charmd is not versioned, but has versioned children.  Versioned directory.
Contents conflict in store/charmd/main.go
Conflict adding files to store/charmload.  Created directory.
Conflict because store/charmload is not versioned, but has versioned children.  Versioned directory.
Contents conflict in store/charmload/main.go
Contents conflict in store/lpad.go
Contents conflict in store/lpad_test.go
Contents conflict in store/server.go
Contents conflict in store/server_test.go
Contents conflict in store/store.go
Contents conflict in store/store_test.go
Conflict adding files to testing.  Created directory.
Conflict because testing is not versioned, but has versioned children.  Versioned directory.
Contents conflict in testing/charm.go
Contents conflict in testing/log.go
Contents conflict in testing/zk_test.go
Conflict adding files to upstart.  Created directory.
Conflict because upstart is not versioned, but has versioned children.  Versioned directory.
Contents conflict in upstart/upstart_test.go
To merge this branch: bzr merge lp:~rogpeppe/pyjuju/go-state-units-under-service
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+108341@code.launchpad.net

Description of the change

state: store units inside their respective services

Making this change simplifies a lot of code - not need
to keep a separate sequence numbering system for units,
for example.

https://codereview.appspot.com/6247066/

To post a comment you must log in.
Revision history for this message
Roger Peppe (rogpeppe) wrote :

Reviewers: mp+108341_code.launchpad.net,

Message:
Please take a look.

Description:
state: store units inside their respective services

Making this change simplifies a lot of code - not need
to keep a separate sequence numbering system for units,
for example.

https://code.launchpad.net/~rogpeppe/juju/go-state-units-under-service/+merge/108341

(do not edit description out of merge proposal)

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

Affected files:
   A [revision details]
   M state/internal_test.go
   M state/machine.go
   M state/service.go
   M state/state.go
   M state/state_test.go
   M state/topology.go
   M state/unit.go

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

LGTM modulo below.

https://codereview.appspot.com/6247066/diff/7001/state/service.go
File state/service.go (right):

https://codereview.appspot.com/6247066/diff/7001/state/service.go#newcode120
state/service.go:120: return zkRemoveTree(s.st.zk, unit.zkPath())
Not relevant to this CL... but do we actually want to do this? The unit
agent will surely still be running; I'm not sure it'll handle this all
that well...

https://codereview.appspot.com/6247066/diff/7001/state/service.go#newcode228
state/service.go:228: return s.zkPath() + "/config"
I strongly approve of all the changes like this one :).

https://codereview.appspot.com/6247066/diff/7001/state/topology.go
File state/topology.go (left):

https://codereview.appspot.com/6247066/diff/7001/state/topology.go#oldcode283
state/topology.go:283: // sequence number will be increased
monotonically for each service.
Update comment

https://codereview.appspot.com/6247066/diff/7001/state/unit.go
File state/unit.go (right):

https://codereview.appspot.com/6247066/diff/7001/state/unit.go#newcode77
state/unit.go:77: func mkUnitKey(serviceKey string, unitId int) string {
What does "mk" signify?

https://codereview.appspot.com/6247066/diff/7001/state/unit.go#newcode86
state/unit.go:86: return "", fmt.Errorf("invalid unit key %q (1) %s",
unitKey, debug.Callers(0, 10))
Is this the proposed implementation or an oversight?

https://codereview.appspot.com/6247066/

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

Very nice change. LGTM, assuming you're happy with the adaptations
below. Otherwise, please reply accordingly and repropose.

Please note you'll have to propose+submit this to the new juju-core
project, since the old trunk is now gone (sorry!). You can do this by
creating a new branch from the new project as you'd usually do, merging
from this work onto it, and then just proposing as usual.

https://codereview.appspot.com/6247066/diff/7001/state/machine.go
File state/machine.go (right):

https://codereview.appspot.com/6247066/diff/7001/state/machine.go#newcode88
state/machine.go:88: // keyId returns the id corresponding to the given
machine or unit id.
s/keyId/keySequence/ or s/keyId/keySeq/

This is not an identifier. "unit-0000001" is the identifier, and we use
the term "key" there, after much debate, precisely to avoid the
confusion with the idea of an "id" which is public. We can easily avoid
the confusion still by calling the sequence number what it really is (it
comes from the idea of sequence from ZooKeeper).

As a special case, we do call the *machine* sequence number an
identifier, because that's how we handle it across the board
(machine.Id(), etc).

https://codereview.appspot.com/6247066/diff/7001/state/service.go
File state/service.go (right):

https://codereview.appspot.com/6247066/diff/7001/state/service.go#newcode13
state/service.go:13: pathPkg "path"
stdpath

https://codereview.appspot.com/6247066/diff/7001/state/service.go#newcode84
state/service.go:84: kprefix = kprefix[:strings.LastIndex(kprefix,
"-")+1]
There's no reason to go over the trouble here. It's already assuming
knowledge about how makeUnitKey works (who said it could skip 'til the
last dash like that?) so it may as well be a lot more clear and say
"/units/unit-" + s.key + "-".

https://codereview.appspot.com/6247066/diff/7001/state/unit.go
File state/unit.go (right):

https://codereview.appspot.com/6247066/diff/7001/state/unit.go#newcode77
state/unit.go:77: func mkUnitKey(serviceKey string, unitId int) string {
On 2012/06/04 07:29:19, fwereade wrote:
> What does "mk" signify?

I assume "make", but I'd also prefer the word itself rather than saving
two chars. Even more when we have serviceKeyForUnitKey below. :-)

https://codereview.appspot.com/6247066/diff/7001/state/unit.go#newcode86
state/unit.go:86: return "", fmt.Errorf("invalid unit key %q (1) %s",
unitKey, debug.Callers(0, 10))
On 2012/06/04 07:29:19, fwereade wrote:
> Is this the proposed implementation or an oversight?

Yeah, looks like an oversight. We need something closer to the error
message of makeUnitKey.

https://codereview.appspot.com/6247066/diff/7001/state/unit.go#newcode518
state/unit.go:518: func parseUnitName(name string) (serviceName string,
serviceId int, err error) {
s/serviceId/serviceSeq/

https://codereview.appspot.com/6247066/diff/7001/state/unit.go#newcode523
state/unit.go:523: id, err := strconv.ParseInt(parts[1], 10, 0)
s/id/seq/

https://codereview.appspot.com/6247066/diff/7001/state/unit.go#newcode527
state/unit.go:527: return parts[0], int(id), nil
s/id/seq/

https://codereview.appspot.com/6247066/

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

I have marked the branch in Launchpad as Rejected, but only because of
the move to juju-core. The above review is still valid and is what
counts.

https://codereview.appspot.com/6247066/

201. By Roger Peppe

state: fixes for review

202. By Roger Peppe

merge trunk

Revision history for this message
Roger Peppe (rogpeppe) wrote :
Download full text (3.7 KiB)

submitted in branch from juju-core

https://codereview.appspot.com/6247066/diff/7001/state/machine.go
File state/machine.go (right):

https://codereview.appspot.com/6247066/diff/7001/state/machine.go#newcode88
state/machine.go:88: // keyId returns the id corresponding to the given
machine or unit id.
On 2012/06/06 20:15:09, niemeyer wrote:
> s/keyId/keySequence/ or s/keyId/keySeq/

> This is not an identifier. "unit-0000001" is the identifier, and we
use the term
> "key" there, after much debate, precisely to avoid the confusion with
the idea
> of an "id" which is public. We can easily avoid the confusion still by
calling
> the sequence number what it really is (it comes from the idea of
sequence from
> ZooKeeper).

> As a special case, we do call the *machine* sequence number an
identifier,
> because that's how we handle it across the board (machine.Id(), etc).

Done.

https://codereview.appspot.com/6247066/diff/7001/state/service.go
File state/service.go (right):

https://codereview.appspot.com/6247066/diff/7001/state/service.go#newcode84
state/service.go:84: kprefix = kprefix[:strings.LastIndex(kprefix,
"-")+1]
On 2012/06/06 20:15:09, niemeyer wrote:
> There's no reason to go over the trouble here. It's already assuming
knowledge
> about how makeUnitKey works (who said it could skip 'til the last dash
like
> that?) so it may as well be a lot more clear and say "/units/unit-" +
s.key +
> "-".

it's not *quite* as simple as that (makeUnitKey strips off the initial
"service-" prefix) but done anyway.

https://codereview.appspot.com/6247066/diff/7001/state/service.go#newcode120
state/service.go:120: return zkRemoveTree(s.st.zk, unit.zkPath())
On 2012/06/04 07:29:19, fwereade wrote:
> Not relevant to this CL... but do we actually want to do this? The
unit agent
> will surely still be running; I'm not sure it'll handle this all that
well...

that's an interesting point (although, as you say, not relevant to this
CL). what *should* we do here? wait until the unit agent has gone away?
leave the directory around and GC it later? or perhaps the best approach
*is* to delete the directory and let the unit agent detect that.

https://codereview.appspot.com/6247066/diff/7001/state/unit.go
File state/unit.go (right):

https://codereview.appspot.com/6247066/diff/7001/state/unit.go#newcode77
state/unit.go:77: func mkUnitKey(serviceKey string, unitId int) string {
On 2012/06/06 20:15:09, niemeyer wrote:
> On 2012/06/04 07:29:19, fwereade wrote:
> > What does "mk" signify?

> I assume "make", but I'd also prefer the word itself rather than
saving two
> chars. Even more when we have serviceKeyForUnitKey below. :-)

done. old C proclivities creeping in there... :-)

https://codereview.appspot.com/6247066/diff/7001/state/unit.go#newcode86
state/unit.go:86: return "", fmt.Errorf("invalid unit key %q (1) %s",
unitKey, debug.Callers(0, 10))
On 2012/06/06 20:15:09, niemeyer wrote:
> On 2012/06/04 07:29:19, fwereade wrote:
> > Is this the proposed implementation or an oversight?

> Yeah, looks like an oversight. We need something closer to the error
message of
> makeUnitKey.

oops. done.

https://codereview.appspot.com/6247066/diff/7001/state/unit.go#newcode518
sta...

Read more...

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

On 2012/06/07 12:07:29, rog wrote:
> On 2012/06/04 07:29:19, fwereade wrote:
> > Not relevant to this CL... but do we actually want to do this? The
unit agent
> > will surely still be running; I'm not sure it'll handle this all
that well...

> that's an interesting point (although, as you say, not relevant to
this CL).
> what *should* we do here? wait until the unit agent has gone away?
leave the
> directory around and GC it later? or perhaps the best approach *is* to
delete
> the directory and let the unit agent detect that.

Offhand, I think I'd favour a dying/dead approach... eg this sets
"dying", which is handled by the UA (which cleans itself up, and sets
"dead"); MA can watch for "dead" on assigned units, tidy them up, and
finally delete the state once we're sure it's unused. Not sure though.

https://codereview.appspot.com/6247066/

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

taking out dirs from running processes is a bad idea. it needs to be
coordinate if your going to leave the process around.

On Thu, Jun 7, 2012 at 11:22 AM, William Reade
<email address hidden>wrote:

> On 2012/06/07 12:07:29, rog wrote:
> > On 2012/06/04 07:29:19, fwereade wrote:
> > > Not relevant to this CL... but do we actually want to do this? The
> unit agent
> > > will surely still be running; I'm not sure it'll handle this all
> that well...
>
> > that's an interesting point (although, as you say, not relevant to
> this CL).
> > what *should* we do here? wait until the unit agent has gone away?
> leave the
> > directory around and GC it later? or perhaps the best approach *is* to
> delete
> > the directory and let the unit agent detect that.
>
> Offhand, I think I'd favour a dying/dead approach... eg this sets
> "dying", which is handled by the UA (which cleans itself up, and sets
> "dead"); MA can watch for "dead" on assigned units, tidy them up, and
> finally delete the state once we're sure it's unused. Not sure though.
>
> https://codereview.appspot.com/6247066/
>
> --
>
> https://code.launchpad.net/~rogpeppe/juju/go-state-units-under-service/+merge/108341
> Your team juju hackers is requested to review the proposed merge of
> lp:~rogpeppe/juju/go-state-units-under-service into lp:juju/go.
>

Unmerged revisions

202. By Roger Peppe

merge trunk

201. By Roger Peppe

state: fixes for review

200. By Roger Peppe

minor tweaks

199. By Roger Peppe

state: use a string for unit key

198. By Roger Peppe

state: move function
;

197. By Roger Peppe

state: move units under service directory

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file '.lbox.OTHER'
2--- .lbox.OTHER 1970-01-01 00:00:00 +0000
3+++ .lbox.OTHER 2012-06-07 11:57:18 +0000
4@@ -0,0 +1,1 @@
5+propose -cr -for lp:juju-core/juju
6
7=== added directory 'charm'
8=== added file 'charm/bundle_test.go.OTHER'
9--- charm/bundle_test.go.OTHER 1970-01-01 00:00:00 +0000
10+++ charm/bundle_test.go.OTHER 2012-06-07 11:57:18 +0000
11@@ -0,0 +1,196 @@
12+package charm_test
13+
14+import (
15+ "bytes"
16+ "fmt"
17+ "io/ioutil"
18+ . "launchpad.net/gocheck"
19+ "launchpad.net/juju-core/juju/charm"
20+ "launchpad.net/juju-core/juju/testing"
21+ "os"
22+ "os/exec"
23+ "path/filepath"
24+)
25+
26+type BundleSuite struct {
27+ repo *testing.Repo
28+ bundlePath string
29+}
30+
31+var _ = Suite(&BundleSuite{})
32+
33+func (s *BundleSuite) SetUpSuite(c *C) {
34+ s.bundlePath = testing.Charms.BundlePath(c.MkDir(), "dummy")
35+}
36+
37+func (s *BundleSuite) TestReadBundle(c *C) {
38+ bundle, err := charm.ReadBundle(s.bundlePath)
39+ c.Assert(err, IsNil)
40+ checkDummy(c, bundle, s.bundlePath)
41+}
42+
43+func (s *BundleSuite) TestReadBundleWithoutConfig(c *C) {
44+ path := testing.Charms.BundlePath(c.MkDir(), "varnish")
45+ bundle, err := charm.ReadBundle(path)
46+ c.Assert(err, IsNil)
47+
48+ // A lacking config.yaml file still causes a proper
49+ // Config value to be returned.
50+ c.Assert(bundle.Config().Options, HasLen, 0)
51+}
52+
53+func (s *BundleSuite) TestReadBundleBytes(c *C) {
54+ data, err := ioutil.ReadFile(s.bundlePath)
55+ c.Assert(err, IsNil)
56+
57+ bundle, err := charm.ReadBundleBytes(data)
58+ c.Assert(err, IsNil)
59+ checkDummy(c, bundle, "")
60+}
61+
62+func (s *BundleSuite) TestExpandTo(c *C) {
63+ bundle, err := charm.ReadBundle(s.bundlePath)
64+ c.Assert(err, IsNil)
65+
66+ path := filepath.Join(c.MkDir(), "charm")
67+ err = bundle.ExpandTo(path)
68+ c.Assert(err, IsNil)
69+
70+ dir, err := charm.ReadDir(path)
71+ c.Assert(err, IsNil)
72+ checkDummy(c, dir, path)
73+}
74+
75+func (s *BundleSuite) TestBundleFileModes(c *C) {
76+ // Apply subtler mode differences than can be expressed in Bazaar.
77+ srcPath := testing.Charms.ClonedDirPath(c.MkDir(), "dummy")
78+ modes := []struct {
79+ path string
80+ mode os.FileMode
81+ }{
82+ {"hooks/install", 0751},
83+ {"empty", 0750},
84+ {"src/hello.c", 0614},
85+ }
86+ for _, m := range modes {
87+ err := os.Chmod(filepath.Join(srcPath, m.path), m.mode)
88+ if err != nil {
89+ panic(err)
90+ }
91+ }
92+
93+ // Bundle and extract the charm to a new directory.
94+ dir, err := charm.ReadDir(srcPath)
95+ c.Assert(err, IsNil)
96+ buf := new(bytes.Buffer)
97+ err = dir.BundleTo(buf)
98+ c.Assert(err, IsNil)
99+ bundle, err := charm.ReadBundleBytes(buf.Bytes())
100+ c.Assert(err, IsNil)
101+ path := c.MkDir()
102+ err = bundle.ExpandTo(path)
103+ c.Assert(err, IsNil)
104+
105+ // Check sensible file modes once round-tripped.
106+ info, err := os.Stat(filepath.Join(path, "src", "hello.c"))
107+ c.Assert(err, IsNil)
108+ c.Assert(info.Mode()&0777, Equals, os.FileMode(0644))
109+ c.Assert(info.Mode()&os.ModeType, Equals, os.FileMode(0))
110+
111+ info, err = os.Stat(filepath.Join(path, "hooks", "install"))
112+ c.Assert(err, IsNil)
113+ c.Assert(info.Mode()&0777, Equals, os.FileMode(0755))
114+ c.Assert(info.Mode()&os.ModeType, Equals, os.FileMode(0))
115+
116+ info, err = os.Stat(filepath.Join(path, "empty"))
117+ c.Assert(err, IsNil)
118+ c.Assert(info.Mode()&0777, Equals, os.FileMode(0755))
119+
120+ target, err := os.Readlink(filepath.Join(path, "hooks", "symlink"))
121+ c.Assert(err, IsNil)
122+ c.Assert(target, Equals, "../target")
123+}
124+
125+func (s *BundleSuite) TestBundleRevisionFile(c *C) {
126+ charmDir := testing.Charms.ClonedDirPath(c.MkDir(), "dummy")
127+ revPath := filepath.Join(charmDir, "revision")
128+
129+ // Missing revision file
130+ err := os.Remove(revPath)
131+ c.Assert(err, IsNil)
132+
133+ bundle, err := charm.ReadBundle(extBundleDir(c, charmDir))
134+ c.Assert(err, IsNil)
135+ c.Assert(bundle.Revision(), Equals, 0)
136+
137+ // Missing revision file with old revision in metadata
138+ file, err := os.OpenFile(filepath.Join(charmDir, "metadata.yaml"), os.O_WRONLY|os.O_APPEND, 0)
139+ c.Assert(err, IsNil)
140+ _, err = file.Write([]byte("\nrevision: 1234\n"))
141+ c.Assert(err, IsNil)
142+
143+ bundle, err = charm.ReadBundle(extBundleDir(c, charmDir))
144+ c.Assert(err, IsNil)
145+ c.Assert(bundle.Revision(), Equals, 1234)
146+
147+ // Revision file with bad content
148+ err = ioutil.WriteFile(revPath, []byte("garbage"), 0666)
149+ c.Assert(err, IsNil)
150+
151+ bundle, err = charm.ReadBundle(extBundleDir(c, charmDir))
152+ c.Assert(err, ErrorMatches, "invalid revision file")
153+ c.Assert(bundle, IsNil)
154+}
155+
156+func (s *BundleSuite) TestBundleSetRevision(c *C) {
157+ bundle, err := charm.ReadBundle(s.bundlePath)
158+ c.Assert(err, IsNil)
159+
160+ c.Assert(bundle.Revision(), Equals, 1)
161+ bundle.SetRevision(42)
162+ c.Assert(bundle.Revision(), Equals, 42)
163+
164+ path := filepath.Join(c.MkDir(), "charm")
165+ err = bundle.ExpandTo(path)
166+ c.Assert(err, IsNil)
167+
168+ dir, err := charm.ReadDir(path)
169+ c.Assert(err, IsNil)
170+ c.Assert(dir.Revision(), Equals, 42)
171+}
172+
173+func (s *BundleSuite) TestExpandToWithBadLink(c *C) {
174+ charmDir := testing.Charms.ClonedDirPath(c.MkDir(), "dummy")
175+ badLink := filepath.Join(charmDir, "hooks", "badlink")
176+
177+ // Symlink targeting a path outside of the charm.
178+ err := os.Symlink("../../target", badLink)
179+ c.Assert(err, IsNil)
180+
181+ bundle, err := charm.ReadBundle(extBundleDir(c, charmDir))
182+ c.Assert(err, IsNil)
183+
184+ path := filepath.Join(c.MkDir(), "charm")
185+ err = bundle.ExpandTo(path)
186+ c.Assert(err, ErrorMatches, `symlink "hooks/badlink" links out of charm: "../../target"`)
187+
188+ // Symlink targeting an absolute path.
189+ os.Remove(badLink)
190+ err = os.Symlink("/target", badLink)
191+ c.Assert(err, IsNil)
192+
193+ bundle, err = charm.ReadBundle(extBundleDir(c, charmDir))
194+ c.Assert(err, IsNil)
195+
196+ path = filepath.Join(c.MkDir(), "charm")
197+ err = bundle.ExpandTo(path)
198+ c.Assert(err, ErrorMatches, `symlink "hooks/badlink" is absolute: "/target"`)
199+}
200+
201+func extBundleDir(c *C, dirpath string) (path string) {
202+ path = filepath.Join(c.MkDir(), "bundle.charm")
203+ cmd := exec.Command("/bin/sh", "-c", fmt.Sprintf("cd %s; zip --fifo --symlinks -r %s .", dirpath, path))
204+ output, err := cmd.CombinedOutput()
205+ c.Assert(err, IsNil, Commentf("Command output: %s", output))
206+ return path
207+}
208
209=== added file 'charm/charm_test.go.OTHER'
210--- charm/charm_test.go.OTHER 1970-01-01 00:00:00 +0000
211+++ charm/charm_test.go.OTHER 2012-06-07 11:57:18 +0000
212@@ -0,0 +1,97 @@
213+package charm_test
214+
215+import (
216+ "bytes"
217+ "io"
218+ "io/ioutil"
219+ . "launchpad.net/gocheck"
220+ "launchpad.net/goyaml"
221+ "launchpad.net/juju-core/juju/charm"
222+ "launchpad.net/juju-core/juju/testing"
223+ stdtesting "testing"
224+)
225+
226+func Test(t *stdtesting.T) {
227+ TestingT(t)
228+}
229+
230+type CharmSuite struct{}
231+
232+var _ = Suite(&CharmSuite{})
233+
234+func (s *CharmSuite) TestRead(c *C) {
235+ bPath := testing.Charms.BundlePath(c.MkDir(), "dummy")
236+ ch, err := charm.Read(bPath)
237+ c.Assert(err, IsNil)
238+ c.Assert(ch.Meta().Name, Equals, "dummy")
239+ dPath := testing.Charms.DirPath("dummy")
240+ ch, err = charm.Read(dPath)
241+ c.Assert(err, IsNil)
242+ c.Assert(ch.Meta().Name, Equals, "dummy")
243+}
244+
245+var inferRepoTests = []struct {
246+ name string
247+ series string
248+ path string
249+ curl string
250+}{
251+ {"wordpress", "precise", "anything", "cs:precise/wordpress"},
252+ {"oneiric/wordpress", "anything", "anything", "cs:oneiric/wordpress"},
253+ {"cs:oneiric/wordpress", "anything", "anything", "cs:oneiric/wordpress"},
254+ {"local:wordpress", "precise", "/some/path", "local:precise/wordpress"},
255+ {"local:oneiric/wordpress", "anything", "/some/path", "local:oneiric/wordpress"},
256+}
257+
258+func (s *CharmSuite) TestInferRepository(c *C) {
259+ for _, t := range inferRepoTests {
260+ repo, curl, err := charm.InferRepository(t.name, t.series, t.path)
261+ c.Assert(err, IsNil)
262+ expectCurl := charm.MustParseURL(t.curl)
263+ c.Assert(curl, DeepEquals, expectCurl)
264+ if localRepo, ok := repo.(*charm.LocalRepository); ok {
265+ c.Assert(localRepo.Path, Equals, t.path)
266+ c.Assert(curl.Schema, Equals, "local")
267+ } else {
268+ c.Assert(curl.Schema, Equals, "cs")
269+ }
270+ }
271+ _, _, err := charm.InferRepository("local:whatever", "series", "")
272+ c.Assert(err, ErrorMatches, "path to local repository not specified")
273+}
274+
275+func checkDummy(c *C, f charm.Charm, path string) {
276+ c.Assert(f.Revision(), Equals, 1)
277+ c.Assert(f.Meta().Name, Equals, "dummy")
278+ c.Assert(f.Config().Options["title"].Default, Equals, "My Title")
279+ switch f := f.(type) {
280+ case *charm.Bundle:
281+ c.Assert(f.Path, Equals, path)
282+
283+ case *charm.Dir:
284+ c.Assert(f.Path, Equals, path)
285+ }
286+}
287+
288+type YamlHacker map[interface{}]interface{}
289+
290+func ReadYaml(r io.Reader) YamlHacker {
291+ data, err := ioutil.ReadAll(r)
292+ if err != nil {
293+ panic(err)
294+ }
295+ m := make(map[interface{}]interface{})
296+ err = goyaml.Unmarshal(data, m)
297+ if err != nil {
298+ panic(err)
299+ }
300+ return YamlHacker(m)
301+}
302+
303+func (yh YamlHacker) Reader() io.Reader {
304+ data, err := goyaml.Marshal(yh)
305+ if err != nil {
306+ panic(err)
307+ }
308+ return bytes.NewBuffer(data)
309+}
310
311=== added file 'charm/config.go.OTHER'
312--- charm/config.go.OTHER 1970-01-01 00:00:00 +0000
313+++ charm/config.go.OTHER 2012-06-07 11:57:18 +0000
314@@ -0,0 +1,141 @@
315+package charm
316+
317+import (
318+ "errors"
319+ "fmt"
320+ "io"
321+ "io/ioutil"
322+ "launchpad.net/goyaml"
323+ "launchpad.net/juju-core/juju/schema"
324+ "reflect"
325+ "strconv"
326+)
327+
328+// Option represents a single configuration option that is declared
329+// as supported by a charm in its config.yaml file.
330+type Option struct {
331+ Title string
332+ Description string
333+ Type string
334+ Default interface{}
335+}
336+
337+// Config represents the supported configuration options for a charm,
338+// as declared in its config.yaml file.
339+type Config struct {
340+ Options map[string]Option
341+}
342+
343+// NewConfig returns a new Config without any options.
344+func NewConfig() *Config {
345+ return &Config{make(map[string]Option)}
346+}
347+
348+// ReadConfig reads a config.yaml file and returns its representation.
349+func ReadConfig(r io.Reader) (config *Config, err error) {
350+ data, err := ioutil.ReadAll(r)
351+ if err != nil {
352+ return
353+ }
354+ raw := make(map[interface{}]interface{})
355+ err = goyaml.Unmarshal(data, raw)
356+ if err != nil {
357+ return
358+ }
359+ v, err := configSchema.Coerce(raw, nil)
360+ if err != nil {
361+ return nil, errors.New("config: " + err.Error())
362+ }
363+ config = NewConfig()
364+ m := v.(schema.MapType)
365+ for name, infov := range m["options"].(schema.MapType) {
366+ opt := infov.(schema.MapType)
367+ optTitle, _ := opt["title"].(string)
368+ optType, _ := opt["type"].(string)
369+ optDescr, _ := opt["description"].(string)
370+ optDefault := opt["default"]
371+ if optDefault != nil {
372+ if reflect.TypeOf(optDefault).Kind() != validTypes[optType] {
373+ msg := "Bad default for %q: %v is not of type %s"
374+ return nil, fmt.Errorf(msg, name, optDefault, optType)
375+ }
376+ }
377+ config.Options[name.(string)] = Option{
378+ Title: optTitle,
379+ Type: optType,
380+ Description: optDescr,
381+ Default: optDefault,
382+ }
383+ }
384+ return
385+}
386+
387+// Validate processes the values in the input map according to the
388+// configuration in config, doing the following operations:
389+//
390+// - Values are converted from strings to the types defined
391+// - Options with default values are introduced for missing keys
392+// - Unknown keys and badly typed values are reported as errors
393+//
394+func (c *Config) Validate(values map[string]string) (processed map[string]interface{}, err error) {
395+ out := make(map[string]interface{})
396+ for k, v := range values {
397+ opt, ok := c.Options[k]
398+ if !ok {
399+ return nil, fmt.Errorf("Unknown configuration option: %q", k)
400+ }
401+ switch opt.Type {
402+ case "string":
403+ out[k] = v
404+ case "int":
405+ i, err := strconv.ParseInt(v, 10, 64)
406+ if err != nil {
407+ return nil, fmt.Errorf("Value for %q is not an int: %q", k, v)
408+ }
409+ out[k] = i
410+ case "float":
411+ f, err := strconv.ParseFloat(v, 64)
412+ if err != nil {
413+ return nil, fmt.Errorf("Value for %q is not a float: %q", k, v)
414+ }
415+ out[k] = f
416+ case "boolean":
417+ b, err := strconv.ParseBool(v)
418+ if err != nil {
419+ return nil, fmt.Errorf("Value for %q is not a boolean: %q", k, v)
420+ }
421+ out[k] = b
422+ default:
423+ panic(fmt.Errorf("Internal error: option type %q is unknown to Validate", opt.Type))
424+ }
425+ }
426+ for k, opt := range c.Options {
427+ if _, ok := out[k]; !ok && opt.Default != nil {
428+ out[k] = opt.Default
429+ }
430+ }
431+ return out, nil
432+}
433+
434+var validTypes = map[string]reflect.Kind{
435+ "string": reflect.String,
436+ "int": reflect.Int64,
437+ "boolean": reflect.Bool,
438+ "float": reflect.Float64,
439+}
440+
441+var optionSchema = schema.FieldMap(
442+ schema.Fields{
443+ "type": schema.OneOf(schema.Const("string"), schema.Const("int"), schema.Const("float"), schema.Const("boolean")),
444+ "default": schema.OneOf(schema.String(), schema.Int(), schema.Float(), schema.Bool()),
445+ "description": schema.String(),
446+ },
447+ schema.Optional{"default", "description"},
448+)
449+
450+var configSchema = schema.FieldMap(
451+ schema.Fields{
452+ "options": schema.Map(schema.String(), optionSchema),
453+ },
454+ nil,
455+)
456
457=== added file 'charm/config_test.go.OTHER'
458--- charm/config_test.go.OTHER 1970-01-01 00:00:00 +0000
459+++ charm/config_test.go.OTHER 2012-06-07 11:57:18 +0000
460@@ -0,0 +1,191 @@
461+package charm_test
462+
463+import (
464+ "bytes"
465+ "fmt"
466+ "io"
467+ "io/ioutil"
468+ . "launchpad.net/gocheck"
469+ "launchpad.net/juju-core/juju/charm"
470+ "launchpad.net/juju-core/juju/testing"
471+ "os"
472+ "path/filepath"
473+)
474+
475+var sampleConfig = `
476+options:
477+ title:
478+ default: My Title
479+ description: A descriptive title used for the service.
480+ type: string
481+ outlook:
482+ description: No default outlook.
483+ type: string
484+ username:
485+ default: admin001
486+ description: The name of the initial account (given admin permissions).
487+ type: string
488+ skill-level:
489+ description: A number indicating skill.
490+ type: int
491+ agility-ratio:
492+ description: A number from 0 to 1 indicating agility.
493+ type: float
494+ reticulate-splines:
495+ description: Whether to reticulate splines on launch, or not.
496+ type: boolean
497+`
498+
499+func repoConfig(name string) io.Reader {
500+ charmDir := testing.Charms.DirPath(name)
501+ file, err := os.Open(filepath.Join(charmDir, "config.yaml"))
502+ if err != nil {
503+ panic(err)
504+ }
505+ defer file.Close()
506+ data, err := ioutil.ReadAll(file)
507+ if err != nil {
508+ panic(err)
509+ }
510+ return bytes.NewBuffer(data)
511+}
512+
513+type ConfigSuite struct{}
514+
515+var _ = Suite(&ConfigSuite{})
516+
517+func (s *ConfigSuite) TestReadConfig(c *C) {
518+ config, err := charm.ReadConfig(repoConfig("dummy"))
519+ c.Assert(err, IsNil)
520+ c.Assert(config.Options["title"], DeepEquals,
521+ charm.Option{
522+ Default: "My Title",
523+ Description: "A descriptive title used for the service.",
524+ Type: "string",
525+ },
526+ )
527+}
528+
529+func (s *ConfigSuite) TestConfigError(c *C) {
530+ _, err := charm.ReadConfig(bytes.NewBuffer([]byte(`options: {t: {type: foo}}`)))
531+ c.Assert(err, ErrorMatches, `config: options.t.type: unsupported value`)
532+}
533+
534+func (s *ConfigSuite) TestDefaultType(c *C) {
535+ assertDefault := func(type_ string, value string, expected interface{}) {
536+ config := fmt.Sprintf(`options: {t: {type: %s, default: %s}}`, type_, value)
537+ result, err := charm.ReadConfig(bytes.NewBuffer([]byte(config)))
538+ c.Assert(err, IsNil)
539+ c.Assert(result.Options["t"].Default, Equals, expected)
540+ }
541+
542+ assertDefault("boolean", "true", true)
543+ assertDefault("string", "golden grahams", "golden grahams")
544+ assertDefault("float", "2.2e11", 2.2e11)
545+ assertDefault("int", "99", int64(99))
546+
547+ assertTypeError := func(type_ string, value string) {
548+ config := fmt.Sprintf(`options: {t: {type: %s, default: %s}}`, type_, value)
549+ _, err := charm.ReadConfig(bytes.NewBuffer([]byte(config)))
550+ expected := fmt.Sprintf(`Bad default for "t": %s is not of type %s`, value, type_)
551+ c.Assert(err, ErrorMatches, expected)
552+ }
553+
554+ assertTypeError("boolean", "henry")
555+ assertTypeError("string", "2.5")
556+ assertTypeError("float", "blob")
557+ assertTypeError("int", "33.2")
558+}
559+
560+func (s *ConfigSuite) TestParseSample(c *C) {
561+ config, err := charm.ReadConfig(bytes.NewBuffer([]byte(sampleConfig)))
562+ c.Assert(err, IsNil)
563+
564+ opt := config.Options
565+ c.Assert(opt["title"], DeepEquals,
566+ charm.Option{
567+ Default: "My Title",
568+ Description: "A descriptive title used for the service.",
569+ Type: "string",
570+ },
571+ )
572+ c.Assert(opt["outlook"], DeepEquals,
573+ charm.Option{
574+ Description: "No default outlook.",
575+ Type: "string",
576+ },
577+ )
578+ c.Assert(opt["username"], DeepEquals,
579+ charm.Option{
580+ Default: "admin001",
581+ Description: "The name of the initial account (given admin permissions).",
582+ Type: "string",
583+ },
584+ )
585+ c.Assert(opt["skill-level"], DeepEquals,
586+ charm.Option{
587+ Description: "A number indicating skill.",
588+ Type: "int",
589+ },
590+ )
591+ c.Assert(opt["reticulate-splines"], DeepEquals,
592+ charm.Option{
593+ Description: "Whether to reticulate splines on launch, or not.",
594+ Type: "boolean",
595+ },
596+ )
597+}
598+
599+func (s *ConfigSuite) TestValidate(c *C) {
600+ config, err := charm.ReadConfig(bytes.NewBuffer([]byte(sampleConfig)))
601+ c.Assert(err, IsNil)
602+
603+ input := map[string]string{
604+ "title": "Helpful Title",
605+ "outlook": "Peachy",
606+ }
607+
608+ // This should include an overridden value, a default and a new value.
609+ expected := map[string]interface{}{
610+ "title": "Helpful Title",
611+ "outlook": "Peachy",
612+ "username": "admin001",
613+ }
614+
615+ output, err := config.Validate(input)
616+ c.Assert(err, IsNil)
617+ c.Assert(output, DeepEquals, expected)
618+
619+ // Check whether float conversion is working.
620+ input["agility-ratio"] = "0.5"
621+ input["skill-level"] = "7"
622+ expected["agility-ratio"] = 0.5
623+ expected["skill-level"] = int64(7)
624+ output, err = config.Validate(input)
625+ c.Assert(err, IsNil)
626+ c.Assert(output, DeepEquals, expected)
627+
628+ // Check whether float errors are caught.
629+ input["agility-ratio"] = "foo"
630+ output, err = config.Validate(input)
631+ c.Assert(err, ErrorMatches, `Value for "agility-ratio" is not a float: "foo"`)
632+ input["agility-ratio"] = "0.5"
633+
634+ // Check whether int errors are caught.
635+ input["skill-level"] = "foo"
636+ output, err = config.Validate(input)
637+ c.Assert(err, ErrorMatches, `Value for "skill-level" is not an int: "foo"`)
638+ input["skill-level"] = "7"
639+
640+ // Check whether boolean errors are caught.
641+ input["reticulate-splines"] = "maybe"
642+ output, err = config.Validate(input)
643+ c.Assert(err, ErrorMatches, `Value for "reticulate-splines" is not a boolean: "maybe"`)
644+ input["reticulate-splines"] = "false"
645+
646+ // Now try to set a value outside the expected.
647+ input["bad"] = "value"
648+ output, err = config.Validate(input)
649+ c.Assert(output, IsNil)
650+ c.Assert(err, ErrorMatches, `Unknown configuration option: "bad"`)
651+}
652
653=== added file 'charm/dir_test.go.OTHER'
654--- charm/dir_test.go.OTHER 1970-01-01 00:00:00 +0000
655+++ charm/dir_test.go.OTHER 2012-06-07 11:57:18 +0000
656@@ -0,0 +1,199 @@
657+package charm_test
658+
659+import (
660+ "archive/zip"
661+ "bytes"
662+ "io/ioutil"
663+ . "launchpad.net/gocheck"
664+ "launchpad.net/juju-core/juju/charm"
665+ "launchpad.net/juju-core/juju/testing"
666+ "os"
667+ "path/filepath"
668+ "syscall"
669+)
670+
671+type DirSuite struct{}
672+
673+var _ = Suite(&DirSuite{})
674+
675+func (s *DirSuite) TestReadDir(c *C) {
676+ path := testing.Charms.DirPath("dummy")
677+ dir, err := charm.ReadDir(path)
678+ c.Assert(err, IsNil)
679+ checkDummy(c, dir, path)
680+}
681+
682+func (s *DirSuite) TestReadDirWithoutConfig(c *C) {
683+ path := testing.Charms.DirPath("varnish")
684+ dir, err := charm.ReadDir(path)
685+ c.Assert(err, IsNil)
686+
687+ // A lacking config.yaml file still causes a proper
688+ // Config value to be returned.
689+ c.Assert(dir.Config().Options, HasLen, 0)
690+}
691+
692+func (s *DirSuite) TestBundleTo(c *C) {
693+ dir := testing.Charms.Dir("dummy")
694+ path := filepath.Join(c.MkDir(), "bundle.charm")
695+ file, err := os.Create(path)
696+ c.Assert(err, IsNil)
697+ err = dir.BundleTo(file)
698+ file.Close()
699+ c.Assert(err, IsNil)
700+
701+ zipr, err := zip.OpenReader(path)
702+ c.Assert(err, IsNil)
703+ defer zipr.Close()
704+
705+ var metaf, instf, emptyf, revf, symf *zip.File
706+ for _, f := range zipr.File {
707+ c.Logf("Bundled file: %s", f.Name)
708+ switch f.Name {
709+ case "revision":
710+ revf = f
711+ case "metadata.yaml":
712+ metaf = f
713+ case "hooks/install":
714+ instf = f
715+ case "hooks/symlink":
716+ symf = f
717+ case "empty/":
718+ emptyf = f
719+ case "build/ignored":
720+ c.Errorf("bundle includes build/*: %s", f.Name)
721+ case ".ignored", ".dir/ignored":
722+ c.Errorf("bundle includes .* entries: %s", f.Name)
723+ }
724+ }
725+
726+ c.Assert(revf, NotNil)
727+ reader, err := revf.Open()
728+ c.Assert(err, IsNil)
729+ data, err := ioutil.ReadAll(reader)
730+ reader.Close()
731+ c.Assert(err, IsNil)
732+ c.Assert(string(data), Equals, "1")
733+
734+ c.Assert(metaf, NotNil)
735+ reader, err = metaf.Open()
736+ c.Assert(err, IsNil)
737+ meta, err := charm.ReadMeta(reader)
738+ reader.Close()
739+ c.Assert(err, IsNil)
740+ c.Assert(meta.Name, Equals, "dummy")
741+
742+ c.Assert(instf, NotNil)
743+ // Despite it being 0751, we pack and unpack it as 0755.
744+ c.Assert(instf.Mode()&0777, Equals, os.FileMode(0755))
745+
746+ c.Assert(symf, NotNil)
747+ c.Assert(symf.Mode()&0777, Equals, os.FileMode(0777))
748+ reader, err = symf.Open()
749+ c.Assert(err, IsNil)
750+ data, err = ioutil.ReadAll(reader)
751+ reader.Close()
752+ c.Assert(err, IsNil)
753+ c.Assert(string(data), Equals, "../target")
754+
755+ c.Assert(emptyf, NotNil)
756+ c.Assert(emptyf.Mode()&os.ModeType, Equals, os.ModeDir)
757+ // Despite it being 0750, we pack and unpack it as 0755.
758+ c.Assert(emptyf.Mode()&0777, Equals, os.FileMode(0755))
759+}
760+
761+func (s *DirSuite) TestBundleToWithBadType(c *C) {
762+ charmDir := testing.Charms.ClonedDirPath(c.MkDir(), "dummy")
763+ badFile := filepath.Join(charmDir, "hooks", "badfile")
764+
765+ // Symlink targeting a path outside of the charm.
766+ err := os.Symlink("../../target", badFile)
767+ c.Assert(err, IsNil)
768+
769+ dir, err := charm.ReadDir(charmDir)
770+ c.Assert(err, IsNil)
771+
772+ err = dir.BundleTo(&bytes.Buffer{})
773+ c.Assert(err, ErrorMatches, `symlink "hooks/badfile" links out of charm: "../../target"`)
774+
775+ // Symlink targeting an absolute path.
776+ os.Remove(badFile)
777+ err = os.Symlink("/target", badFile)
778+ c.Assert(err, IsNil)
779+
780+ dir, err = charm.ReadDir(charmDir)
781+ c.Assert(err, IsNil)
782+
783+ err = dir.BundleTo(&bytes.Buffer{})
784+ c.Assert(err, ErrorMatches, `symlink "hooks/badfile" is absolute: "/target"`)
785+
786+ // Can't bundle special files either.
787+ os.Remove(badFile)
788+ err = syscall.Mkfifo(badFile, 0644)
789+ c.Assert(err, IsNil)
790+
791+ dir, err = charm.ReadDir(charmDir)
792+ c.Assert(err, IsNil)
793+
794+ err = dir.BundleTo(&bytes.Buffer{})
795+ c.Assert(err, ErrorMatches, `file is a named pipe: "hooks/badfile"`)
796+}
797+
798+func (s *DirSuite) TestDirRevisionFile(c *C) {
799+ charmDir := testing.Charms.ClonedDirPath(c.MkDir(), "dummy")
800+ revPath := filepath.Join(charmDir, "revision")
801+
802+ // Missing revision file
803+ err := os.Remove(revPath)
804+ c.Assert(err, IsNil)
805+
806+ dir, err := charm.ReadDir(charmDir)
807+ c.Assert(err, IsNil)
808+ c.Assert(dir.Revision(), Equals, 0)
809+
810+ // Missing revision file with old revision in metadata
811+ file, err := os.OpenFile(filepath.Join(charmDir, "metadata.yaml"), os.O_WRONLY|os.O_APPEND, 0)
812+ c.Assert(err, IsNil)
813+ _, err = file.Write([]byte("\nrevision: 1234\n"))
814+ c.Assert(err, IsNil)
815+
816+ dir, err = charm.ReadDir(charmDir)
817+ c.Assert(err, IsNil)
818+ c.Assert(dir.Revision(), Equals, 1234)
819+
820+ // Revision file with bad content
821+ err = ioutil.WriteFile(revPath, []byte("garbage"), 0666)
822+ c.Assert(err, IsNil)
823+
824+ dir, err = charm.ReadDir(charmDir)
825+ c.Assert(err, ErrorMatches, "invalid revision file")
826+ c.Assert(dir, IsNil)
827+}
828+
829+func (s *DirSuite) TestDirSetRevision(c *C) {
830+ dir := testing.Charms.ClonedDir(c.MkDir(), "dummy")
831+ c.Assert(dir.Revision(), Equals, 1)
832+ dir.SetRevision(42)
833+ c.Assert(dir.Revision(), Equals, 42)
834+
835+ var b bytes.Buffer
836+ err := dir.BundleTo(&b)
837+ c.Assert(err, IsNil)
838+
839+ bundle, err := charm.ReadBundleBytes(b.Bytes())
840+ c.Assert(bundle.Revision(), Equals, 42)
841+}
842+
843+func (s *DirSuite) TestDirSetDiskRevision(c *C) {
844+ charmDir := testing.Charms.ClonedDirPath(c.MkDir(), "dummy")
845+ dir, err := charm.ReadDir(charmDir)
846+ c.Assert(err, IsNil)
847+
848+ c.Assert(dir.Revision(), Equals, 1)
849+ dir.SetDiskRevision(42)
850+ c.Assert(dir.Revision(), Equals, 42)
851+
852+ dir, err = charm.ReadDir(charmDir)
853+ c.Assert(err, IsNil)
854+ c.Assert(dir.Revision(), Equals, 42)
855+}
856
857=== added file 'charm/export_test.go.OTHER'
858--- charm/export_test.go.OTHER 1970-01-01 00:00:00 +0000
859+++ charm/export_test.go.OTHER 2012-06-07 11:57:18 +0000
860@@ -0,0 +1,15 @@
861+package charm
862+
863+import (
864+ "launchpad.net/juju-core/juju/schema"
865+)
866+
867+// Export meaningful bits for tests only.
868+
869+func IfaceExpander(limit interface{}) schema.Checker {
870+ return ifaceExpander(limit)
871+}
872+
873+func NewStore(url, path string) Repository {
874+ return &store{url, path}
875+}
876
877=== added file 'charm/meta.go.OTHER'
878--- charm/meta.go.OTHER 1970-01-01 00:00:00 +0000
879+++ charm/meta.go.OTHER 2012-06-07 11:57:18 +0000
880@@ -0,0 +1,201 @@
881+package charm
882+
883+import (
884+ "errors"
885+ "fmt"
886+ "io"
887+ "io/ioutil"
888+ "launchpad.net/goyaml"
889+ "launchpad.net/juju-core/juju/schema"
890+)
891+
892+const (
893+ ScopeGlobal = "global"
894+ ScopeContainer = "container"
895+)
896+
897+// Relation represents a single relation defined in the charm
898+// metadata.yaml file.
899+type Relation struct {
900+ Interface string
901+ Optional bool
902+ Limit int
903+ Scope string
904+}
905+
906+// Meta represents all the known content that may be defined
907+// within a charm's metadata.yaml file.
908+type Meta struct {
909+ Name string
910+ Summary string
911+ Description string
912+ Provides map[string]Relation
913+ Requires map[string]Relation
914+ Peers map[string]Relation
915+ OldRevision int // Obsolete
916+ Subordinate bool
917+}
918+
919+// ReadMeta reads the content of a metadata.yaml file and returns
920+// its representation.
921+func ReadMeta(r io.Reader) (meta *Meta, err error) {
922+ data, err := ioutil.ReadAll(r)
923+ if err != nil {
924+ return
925+ }
926+ raw := make(map[interface{}]interface{})
927+ err = goyaml.Unmarshal(data, raw)
928+ if err != nil {
929+ return
930+ }
931+ v, err := charmSchema.Coerce(raw, nil)
932+ if err != nil {
933+ return nil, errors.New("metadata: " + err.Error())
934+ }
935+ m := v.(schema.MapType)
936+ meta = &Meta{}
937+ meta.Name = m["name"].(string)
938+ // Schema decodes as int64, but the int range should be good
939+ // enough for revisions.
940+ meta.Summary = m["summary"].(string)
941+ meta.Description = m["description"].(string)
942+ meta.Provides = parseRelations(m["provides"])
943+ meta.Requires = parseRelations(m["requires"])
944+ meta.Peers = parseRelations(m["peers"])
945+ if rev := m["revision"]; rev != nil {
946+ // Obsolete
947+ meta.OldRevision = int(m["revision"].(int64))
948+ }
949+ // Subordinate charms must have at least one relation that
950+ // has container scope, otherwise they can't relate to the
951+ // principal.
952+ if subordinate := m["subordinate"]; subordinate != nil {
953+ valid := false
954+ if meta.Requires != nil {
955+ for _, relationData := range meta.Requires {
956+ if relationData.Scope == ScopeContainer {
957+ valid = true
958+ break
959+ }
960+ }
961+ }
962+ if !valid {
963+ return nil, fmt.Errorf("subordinate charm %q lacks requires relation with container scope", meta.Name)
964+ }
965+ meta.Subordinate = m["subordinate"].(bool)
966+ }
967+ return
968+}
969+
970+func parseRelations(relations interface{}) map[string]Relation {
971+ if relations == nil {
972+ return nil
973+ }
974+ result := make(map[string]Relation)
975+ for name, rel := range relations.(schema.MapType) {
976+ relMap := rel.(schema.MapType)
977+ relation := Relation{}
978+ relation.Interface = relMap["interface"].(string)
979+ relation.Optional = relMap["optional"].(bool)
980+ if scope := relMap["scope"]; scope != nil {
981+ relation.Scope = scope.(string)
982+ }
983+ if relMap["limit"] != nil {
984+ // Schema defaults to int64, but we know
985+ // the int range should be more than enough.
986+ relation.Limit = int(relMap["limit"].(int64))
987+ }
988+ result[name.(string)] = relation
989+ }
990+ return result
991+}
992+
993+// Schema coercer that expands the interface shorthand notation.
994+// A consistent format is easier to work with than considering the
995+// potential difference everywhere.
996+//
997+// Supports the following variants::
998+//
999+// provides:
1000+// server: riak
1001+// admin: http
1002+// foobar:
1003+// interface: blah
1004+//
1005+// provides:
1006+// server:
1007+// interface: mysql
1008+// limit:
1009+// optional: false
1010+//
1011+// In all input cases, the output is the fully specified interface
1012+// representation as seen in the mysql interface description above.
1013+func ifaceExpander(limit interface{}) schema.Checker {
1014+ return ifaceExpC{limit}
1015+}
1016+
1017+type ifaceExpC struct {
1018+ limit interface{}
1019+}
1020+
1021+var (
1022+ stringC = schema.String()
1023+ mapC = schema.Map(schema.String(), schema.Any())
1024+)
1025+
1026+func (c ifaceExpC) Coerce(v interface{}, path []string) (newv interface{}, err error) {
1027+ s, err := stringC.Coerce(v, path)
1028+ if err == nil {
1029+ newv = schema.MapType{
1030+ "interface": s,
1031+ "limit": c.limit,
1032+ "optional": false,
1033+ "scope": ScopeGlobal,
1034+ }
1035+ return
1036+ }
1037+
1038+ // Optional values are context-sensitive and/or have
1039+ // defaults, which is different than what KeyDict can
1040+ // readily support. So just do it here first, then
1041+ // coerce to the real schema.
1042+ v, err = mapC.Coerce(v, path)
1043+ if err != nil {
1044+ return
1045+ }
1046+ m := v.(schema.MapType)
1047+ if _, ok := m["limit"]; !ok {
1048+ m["limit"] = c.limit
1049+ }
1050+ if _, ok := m["optional"]; !ok {
1051+ m["optional"] = false
1052+ }
1053+ if _, ok := m["scope"]; !ok {
1054+ m["scope"] = ScopeGlobal
1055+ }
1056+ return ifaceSchema.Coerce(m, path)
1057+}
1058+
1059+var ifaceSchema = schema.FieldMap(
1060+ schema.Fields{
1061+ "interface": schema.String(),
1062+ "limit": schema.OneOf(schema.Const(nil), schema.Int()),
1063+ "scope": schema.OneOf(schema.Const(ScopeGlobal), schema.Const(ScopeContainer)),
1064+ "optional": schema.Bool(),
1065+ },
1066+ schema.Optional{"scope"},
1067+)
1068+
1069+var charmSchema = schema.FieldMap(
1070+ schema.Fields{
1071+ "name": schema.String(),
1072+ "summary": schema.String(),
1073+ "description": schema.String(),
1074+ "peers": schema.Map(schema.String(), ifaceExpander(1)),
1075+ "provides": schema.Map(schema.String(), ifaceExpander(nil)),
1076+ "requires": schema.Map(schema.String(), ifaceExpander(1)),
1077+ "revision": schema.Int(), // Obsolete
1078+ "subordinate": schema.Bool(),
1079+ },
1080+ schema.Optional{"provides", "requires", "peers", "revision", "subordinate"},
1081+)
1082
1083=== added file 'charm/meta_test.go.OTHER'
1084--- charm/meta_test.go.OTHER 1970-01-01 00:00:00 +0000
1085+++ charm/meta_test.go.OTHER 2012-06-07 11:57:18 +0000
1086@@ -0,0 +1,137 @@
1087+package charm_test
1088+
1089+import (
1090+ "bytes"
1091+ "io"
1092+ "io/ioutil"
1093+ . "launchpad.net/gocheck"
1094+ "launchpad.net/juju-core/juju/charm"
1095+ "launchpad.net/juju-core/juju/schema"
1096+ "launchpad.net/juju-core/juju/testing"
1097+ "os"
1098+ "path/filepath"
1099+)
1100+
1101+func repoMeta(name string) io.Reader {
1102+ charmDir := testing.Charms.DirPath(name)
1103+ file, err := os.Open(filepath.Join(charmDir, "metadata.yaml"))
1104+ if err != nil {
1105+ panic(err)
1106+ }
1107+ defer file.Close()
1108+ data, err := ioutil.ReadAll(file)
1109+ if err != nil {
1110+ panic(err)
1111+ }
1112+ return bytes.NewBuffer(data)
1113+}
1114+
1115+type MetaSuite struct{}
1116+
1117+var _ = Suite(&MetaSuite{})
1118+
1119+func (s *MetaSuite) TestReadMeta(c *C) {
1120+ meta, err := charm.ReadMeta(repoMeta("dummy"))
1121+ c.Assert(err, IsNil)
1122+ c.Assert(meta.Name, Equals, "dummy")
1123+ c.Assert(meta.Summary, Equals, "That's a dummy charm.")
1124+ c.Assert(meta.Description, Equals,
1125+ "This is a longer description which\npotentially contains multiple lines.\n")
1126+ c.Assert(meta.OldRevision, Equals, 0)
1127+ c.Assert(meta.Subordinate, Equals, false)
1128+}
1129+
1130+func (s *MetaSuite) TestSubordinate(c *C) {
1131+ meta, err := charm.ReadMeta(repoMeta("logging"))
1132+ c.Assert(err, IsNil)
1133+ c.Assert(meta.Subordinate, Equals, true)
1134+}
1135+
1136+func (s *MetaSuite) TestSubordinateWithoutContainerRelation(c *C) {
1137+ r := repoMeta("dummy")
1138+ hackYaml := ReadYaml(r)
1139+ hackYaml["subordinate"] = true
1140+ _, err := charm.ReadMeta(hackYaml.Reader())
1141+ c.Assert(err, ErrorMatches, "subordinate charm \"dummy\" lacks requires relation with container scope")
1142+}
1143+
1144+func (s *MetaSuite) TestScopeConstraint(c *C) {
1145+ meta, err := charm.ReadMeta(repoMeta("logging"))
1146+ c.Assert(err, IsNil)
1147+ c.Assert(meta.Provides["logging-client"].Scope, Equals, charm.ScopeGlobal)
1148+ c.Assert(meta.Requires["logging-directory"].Scope, Equals, charm.ScopeContainer)
1149+ c.Assert(meta.Subordinate, Equals, true)
1150+}
1151+
1152+func (s *MetaSuite) TestParseMetaRelations(c *C) {
1153+ meta, err := charm.ReadMeta(repoMeta("mysql"))
1154+ c.Assert(err, IsNil)
1155+ c.Assert(meta.Provides["server"], Equals, charm.Relation{Interface: "mysql", Scope: charm.ScopeGlobal})
1156+ c.Assert(meta.Requires, IsNil)
1157+ c.Assert(meta.Peers, IsNil)
1158+
1159+ meta, err = charm.ReadMeta(repoMeta("riak"))
1160+ c.Assert(err, IsNil)
1161+ c.Assert(meta.Provides["endpoint"], Equals, charm.Relation{Interface: "http", Scope: charm.ScopeGlobal})
1162+ c.Assert(meta.Provides["admin"], Equals, charm.Relation{Interface: "http", Scope: charm.ScopeGlobal})
1163+ c.Assert(meta.Peers["ring"], Equals, charm.Relation{Interface: "riak", Limit: 1, Scope: charm.ScopeGlobal})
1164+ c.Assert(meta.Requires, IsNil)
1165+
1166+ meta, err = charm.ReadMeta(repoMeta("wordpress"))
1167+ c.Assert(err, IsNil)
1168+ c.Assert(meta.Provides["url"], Equals, charm.Relation{Interface: "http", Scope: charm.ScopeGlobal})
1169+ c.Assert(meta.Requires["db"], Equals, charm.Relation{Interface: "mysql", Limit: 1, Scope: charm.ScopeGlobal})
1170+ c.Assert(meta.Requires["cache"], Equals, charm.Relation{Interface: "varnish", Limit: 2, Optional: true, Scope: charm.ScopeGlobal})
1171+ c.Assert(meta.Peers, IsNil)
1172+
1173+}
1174+
1175+// Test rewriting of a given interface specification into long form.
1176+//
1177+// InterfaceExpander uses `coerce` to do one of two things:
1178+//
1179+// - Rewrite shorthand to the long form used for actual storage
1180+// - Fills in defaults, including a configurable `limit`
1181+//
1182+// This test ensures test coverage on each of these branches, along
1183+// with ensuring the conversion object properly raises SchemaError
1184+// exceptions on invalid data.
1185+func (s *MetaSuite) TestIfaceExpander(c *C) {
1186+ e := charm.IfaceExpander(nil)
1187+
1188+ path := []string{"<pa", "th>"}
1189+
1190+ // Shorthand is properly rewritten
1191+ v, err := e.Coerce("http", path)
1192+ c.Assert(err, IsNil)
1193+ c.Assert(v, DeepEquals, schema.MapType{"interface": "http", "limit": nil, "optional": false, "scope": charm.ScopeGlobal})
1194+
1195+ // Defaults are properly applied
1196+ v, err = e.Coerce(schema.MapType{"interface": "http"}, path)
1197+ c.Assert(err, IsNil)
1198+ c.Assert(v, DeepEquals, schema.MapType{"interface": "http", "limit": nil, "optional": false, "scope": charm.ScopeGlobal})
1199+
1200+ v, err = e.Coerce(schema.MapType{"interface": "http", "limit": 2}, path)
1201+ c.Assert(err, IsNil)
1202+ c.Assert(v, DeepEquals, schema.MapType{"interface": "http", "limit": int64(2), "optional": false, "scope": charm.ScopeGlobal})
1203+
1204+ v, err = e.Coerce(schema.MapType{"interface": "http", "optional": true}, path)
1205+ c.Assert(err, IsNil)
1206+ c.Assert(v, DeepEquals, schema.MapType{"interface": "http", "limit": nil, "optional": true, "scope": charm.ScopeGlobal})
1207+
1208+ // Invalid data raises an error.
1209+ v, err = e.Coerce(42, path)
1210+ c.Assert(err, ErrorMatches, "<path>: expected map, got 42")
1211+
1212+ v, err = e.Coerce(schema.MapType{"interface": "http", "optional": nil}, path)
1213+ c.Assert(err, ErrorMatches, "<path>.optional: expected bool, got nothing")
1214+
1215+ v, err = e.Coerce(schema.MapType{"interface": "http", "limit": "none, really"}, path)
1216+ c.Assert(err, ErrorMatches, "<path>.limit: unsupported value")
1217+
1218+ // Can change default limit
1219+ e = charm.IfaceExpander(1)
1220+ v, err = e.Coerce(schema.MapType{"interface": "http"}, path)
1221+ c.Assert(err, IsNil)
1222+ c.Assert(v, DeepEquals, schema.MapType{"interface": "http", "limit": int64(1), "optional": false, "scope": charm.ScopeGlobal})
1223+}
1224
1225=== added file 'charm/repo.go.OTHER'
1226--- charm/repo.go.OTHER 1970-01-01 00:00:00 +0000
1227+++ charm/repo.go.OTHER 2012-06-07 11:57:18 +0000
1228@@ -0,0 +1,226 @@
1229+package charm
1230+
1231+import (
1232+ "crypto/sha256"
1233+ "encoding/hex"
1234+ "encoding/json"
1235+ "fmt"
1236+ "io"
1237+ "io/ioutil"
1238+ "launchpad.net/juju-core/juju/log"
1239+ "net/http"
1240+ "net/url"
1241+ "os"
1242+ "path/filepath"
1243+ "strings"
1244+)
1245+
1246+// InfoResponse is sent by the charm store in response to charm-info requests.
1247+type InfoResponse struct {
1248+ Revision int `json:"revision"` // Zero is valid. Can't omitempty.
1249+ Sha256 string `json:"sha256,omitempty"`
1250+ Errors []string `json:"errors,omitempty"`
1251+ Warnings []string `json:"warnings,omitempty"`
1252+}
1253+
1254+// Repository respresents a collection of charms.
1255+type Repository interface {
1256+ Get(curl *URL) (Charm, error)
1257+ Latest(curl *URL) (int, error)
1258+}
1259+
1260+// store is a Repository that talks to the juju charm server (in ../store).
1261+type store struct {
1262+ baseURL string
1263+ cachePath string
1264+}
1265+
1266+const (
1267+ storeURL = "https://store.juju.ubuntu.com"
1268+ cachePath = "$HOME/.juju/cache"
1269+)
1270+
1271+// Store returns a Repository that provides access to the juju charm store.
1272+func Store() Repository {
1273+ return &store{storeURL, os.ExpandEnv(cachePath)}
1274+}
1275+
1276+// info returns the revision and SHA256 digest of the charm referenced by curl.
1277+func (s *store) info(curl *URL) (rev int, digest string, err error) {
1278+ key := curl.String()
1279+ resp, err := http.Get(s.baseURL + "/charm-info?charms=" + url.QueryEscape(key))
1280+ if err != nil {
1281+ return
1282+ }
1283+ defer resp.Body.Close()
1284+ body, err := ioutil.ReadAll(resp.Body)
1285+ if err != nil {
1286+ return
1287+ }
1288+ infos := make(map[string]*InfoResponse)
1289+ if err = json.Unmarshal(body, &infos); err != nil {
1290+ return
1291+ }
1292+ info, found := infos[key]
1293+ if !found {
1294+ err = fmt.Errorf("charm: charm store returned response without charm %q", key)
1295+ return
1296+ }
1297+ for _, w := range info.Warnings {
1298+ log.Printf("WARNING: charm store reports for %q: %s", key, w)
1299+ }
1300+ if info.Errors != nil {
1301+ err = fmt.Errorf(
1302+ "charm info errors for %q: %s", key, strings.Join(info.Errors, "; "),
1303+ )
1304+ return
1305+ }
1306+ return info.Revision, info.Sha256, nil
1307+}
1308+
1309+// Latest returns the latest revision of the charm referenced by curl, regardless
1310+// of the revision set on curl itself.
1311+func (s *store) Latest(curl *URL) (int, error) {
1312+ rev, _, err := s.info(curl.WithRevision(-1))
1313+ return rev, err
1314+}
1315+
1316+// verify returns an error unless a file exists at path with a hex-encoded
1317+// SHA256 matching digest.
1318+func verify(path, digest string) error {
1319+ b, err := ioutil.ReadFile(path)
1320+ if err != nil {
1321+ return err
1322+ }
1323+ h := sha256.New()
1324+ h.Write(b)
1325+ if hex.EncodeToString(h.Sum(nil)) != digest {
1326+ return fmt.Errorf("bad SHA256 of %q", path)
1327+ }
1328+ return nil
1329+}
1330+
1331+// Get returns the charm referenced by curl.
1332+func (s *store) Get(curl *URL) (Charm, error) {
1333+ if err := os.MkdirAll(s.cachePath, 0755); err != nil {
1334+ return nil, err
1335+ }
1336+ rev, digest, err := s.info(curl)
1337+ if err != nil {
1338+ return nil, err
1339+ }
1340+ if curl.Revision == -1 {
1341+ curl = curl.WithRevision(rev)
1342+ } else if curl.Revision != rev {
1343+ return nil, fmt.Errorf("charm: store returned charm with wrong revision for %q", curl.String())
1344+ }
1345+ path := filepath.Join(s.cachePath, Quote(curl.String())+".charm")
1346+ if verify(path, digest) != nil {
1347+ resp, err := http.Get(s.baseURL + "/charm/" + url.QueryEscape(curl.Path()))
1348+ if err != nil {
1349+ return nil, err
1350+ }
1351+ defer resp.Body.Close()
1352+ f, err := ioutil.TempFile(s.cachePath, "charm-download")
1353+ if err != nil {
1354+ return nil, err
1355+ }
1356+ dlPath := f.Name()
1357+ _, err = io.Copy(f, resp.Body)
1358+ if cerr := f.Close(); err == nil {
1359+ err = cerr
1360+ }
1361+ if err != nil {
1362+ os.Remove(dlPath)
1363+ return nil, err
1364+ }
1365+ if err := os.Rename(dlPath, path); err != nil {
1366+ return nil, err
1367+ }
1368+ }
1369+ if err := verify(path, digest); err != nil {
1370+ return nil, err
1371+ }
1372+ return ReadBundle(path)
1373+}
1374+
1375+// LocalRepository represents a local directory containing subdirectories
1376+// named after an Ubuntu series, each of which contains charms targeted for
1377+// that series. For example:
1378+//
1379+// /path/to/repository/oneiric/mongodb/
1380+// /path/to/repository/precise/mongodb.charm
1381+// /path/to/repository/precise/wordpress/
1382+type LocalRepository struct {
1383+ Path string
1384+}
1385+
1386+// Latest returns the latest revision of the charm referenced by curl, regardless
1387+// of the revision set on curl itself.
1388+func (r *LocalRepository) Latest(curl *URL) (int, error) {
1389+ ch, err := r.Get(curl.WithRevision(-1))
1390+ if err != nil {
1391+ return 0, err
1392+ }
1393+ return ch.Revision(), nil
1394+}
1395+
1396+func repoNotFound(path string) error {
1397+ return fmt.Errorf("no repository found at %q", path)
1398+}
1399+
1400+func charmNotFound(curl *URL) error {
1401+ return fmt.Errorf("no charms found matching %q", curl)
1402+}
1403+
1404+func mightBeCharm(info os.FileInfo) bool {
1405+ if info.IsDir() {
1406+ return !strings.HasPrefix(info.Name(), ".")
1407+ }
1408+ return strings.HasSuffix(info.Name(), ".charm")
1409+}
1410+
1411+// Get returns a charm matching curl, if one exists. If curl has a revision of
1412+// -1, it returns the latest charm that matches curl. If multiple candidates
1413+// satisfy the foregoing, the first one encountered will be returned.
1414+func (r *LocalRepository) Get(curl *URL) (Charm, error) {
1415+ if curl.Schema != "local" {
1416+ return nil, fmt.Errorf("local repository got URL with non-local schema: %q", curl)
1417+ }
1418+ info, err := os.Stat(r.Path)
1419+ if err != nil {
1420+ if os.IsNotExist(err) {
1421+ err = repoNotFound(r.Path)
1422+ }
1423+ return nil, err
1424+ }
1425+ if !info.IsDir() {
1426+ return nil, repoNotFound(r.Path)
1427+ }
1428+ path := filepath.Join(r.Path, curl.Series)
1429+ infos, err := ioutil.ReadDir(path)
1430+ if err != nil {
1431+ return nil, charmNotFound(curl)
1432+ }
1433+ var latest Charm
1434+ for _, info := range infos {
1435+ if !mightBeCharm(info) {
1436+ continue
1437+ }
1438+ chPath := filepath.Join(path, info.Name())
1439+ if ch, err := Read(chPath); err != nil {
1440+ log.Printf("WARNING: failed to load charm at %q: %s", chPath, err)
1441+ } else if ch.Meta().Name == curl.Name {
1442+ if ch.Revision() == curl.Revision {
1443+ return ch, nil
1444+ }
1445+ if latest == nil || ch.Revision() > latest.Revision() {
1446+ latest = ch
1447+ }
1448+ }
1449+ }
1450+ if curl.Revision == -1 && latest != nil {
1451+ return latest, nil
1452+ }
1453+ return nil, charmNotFound(curl)
1454+}
1455
1456=== added file 'charm/repo_test.go.OTHER'
1457--- charm/repo_test.go.OTHER 1970-01-01 00:00:00 +0000
1458+++ charm/repo_test.go.OTHER 2012-06-07 11:57:18 +0000
1459@@ -0,0 +1,339 @@
1460+package charm_test
1461+
1462+import (
1463+ "crypto/sha256"
1464+ "encoding/hex"
1465+ "encoding/json"
1466+ "io/ioutil"
1467+ . "launchpad.net/gocheck"
1468+ "launchpad.net/juju-core/juju/charm"
1469+ "launchpad.net/juju-core/juju/log"
1470+ "launchpad.net/juju-core/juju/testing"
1471+ "net"
1472+ "net/http"
1473+ "os"
1474+ "path/filepath"
1475+ "strconv"
1476+)
1477+
1478+type MockStore struct {
1479+ mux *http.ServeMux
1480+ lis net.Listener
1481+ bundleBytes []byte
1482+ bundleSha256 string
1483+ downloads []*charm.URL
1484+}
1485+
1486+func NewMockStore(c *C) *MockStore {
1487+ s := &MockStore{}
1488+ bytes, err := ioutil.ReadFile(testing.Charms.BundlePath(c.MkDir(), "dummy"))
1489+ c.Assert(err, IsNil)
1490+ s.bundleBytes = bytes
1491+ h := sha256.New()
1492+ h.Write(bytes)
1493+ s.bundleSha256 = hex.EncodeToString(h.Sum(nil))
1494+ s.mux = http.NewServeMux()
1495+ s.mux.HandleFunc("/charm-info", func(w http.ResponseWriter, r *http.Request) {
1496+ s.ServeInfo(w, r)
1497+ })
1498+ s.mux.HandleFunc("/charm/", func(w http.ResponseWriter, r *http.Request) {
1499+ s.ServeCharm(w, r)
1500+ })
1501+ lis, err := net.Listen("tcp", "127.0.0.1:4444")
1502+ c.Assert(err, IsNil)
1503+ s.lis = lis
1504+ go http.Serve(s.lis, s)
1505+ return s
1506+}
1507+
1508+func (s *MockStore) ServeHTTP(w http.ResponseWriter, r *http.Request) {
1509+ s.mux.ServeHTTP(w, r)
1510+}
1511+
1512+func (s *MockStore) ServeInfo(w http.ResponseWriter, r *http.Request) {
1513+ r.ParseForm()
1514+ response := map[string]*charm.InfoResponse{}
1515+ for _, url := range r.Form["charms"] {
1516+ cr := &charm.InfoResponse{}
1517+ response[url] = cr
1518+ curl := charm.MustParseURL(url)
1519+ switch curl.Name {
1520+ case "borken":
1521+ cr.Errors = append(cr.Errors, "badness")
1522+ continue
1523+ case "unwise":
1524+ cr.Warnings = append(cr.Warnings, "foolishness")
1525+ fallthrough
1526+ default:
1527+ if curl.Revision == -1 {
1528+ cr.Revision = 23
1529+ } else {
1530+ cr.Revision = curl.Revision
1531+ }
1532+ cr.Sha256 = s.bundleSha256
1533+ }
1534+ }
1535+ data, err := json.Marshal(response)
1536+ if err != nil {
1537+ panic(err)
1538+ }
1539+ w.Header().Set("Content-Type", "application/json")
1540+ _, err = w.Write(data)
1541+ if err != nil {
1542+ panic(err)
1543+ }
1544+}
1545+
1546+func (s *MockStore) ServeCharm(w http.ResponseWriter, r *http.Request) {
1547+ curl := charm.MustParseURL("cs:" + r.URL.Path[len("/charm/"):])
1548+ s.downloads = append(s.downloads, curl)
1549+ w.Header().Set("Connection", "close")
1550+ w.Header().Set("Content-Type", "application/octet-stream")
1551+ w.Header().Set("Content-Length", strconv.Itoa(len(s.bundleBytes)))
1552+ _, err := w.Write(s.bundleBytes)
1553+ if err != nil {
1554+ panic(err)
1555+ }
1556+}
1557+
1558+type StoreSuite struct {
1559+ server *MockStore
1560+ store charm.Repository
1561+ cache string
1562+}
1563+
1564+var _ = Suite(&StoreSuite{})
1565+
1566+func (s *StoreSuite) SetUpSuite(c *C) {
1567+ s.server = NewMockStore(c)
1568+}
1569+
1570+func (s *StoreSuite) SetUpTest(c *C) {
1571+ s.cache = c.MkDir()
1572+ s.store = charm.NewStore("http://127.0.0.1:4444", s.cache)
1573+ s.server.downloads = nil
1574+}
1575+
1576+func (s *StoreSuite) TearDownSuite(c *C) {
1577+ s.server.lis.Close()
1578+}
1579+
1580+func (s *StoreSuite) TestError(c *C) {
1581+ curl := charm.MustParseURL("cs:series/borken")
1582+ expect := `charm info errors for "cs:series/borken": badness`
1583+ _, err := s.store.Latest(curl)
1584+ c.Assert(err, ErrorMatches, expect)
1585+ _, err = s.store.Get(curl)
1586+ c.Assert(err, ErrorMatches, expect)
1587+}
1588+
1589+func (s *StoreSuite) TestWarning(c *C) {
1590+ orig := log.Target
1591+ log.Target = c
1592+ defer func() { log.Target = orig }()
1593+ curl := charm.MustParseURL("cs:series/unwise")
1594+ expect := `.* JUJU WARNING: charm store reports for "cs:series/unwise": foolishness` + "\n"
1595+ r, err := s.store.Latest(curl)
1596+ c.Assert(r, Equals, 23)
1597+ c.Assert(err, IsNil)
1598+ c.Assert(c.GetTestLog(), Matches, expect)
1599+ ch, err := s.store.Get(curl)
1600+ c.Assert(ch, NotNil)
1601+ c.Assert(err, IsNil)
1602+ c.Assert(c.GetTestLog(), Matches, expect+expect)
1603+}
1604+
1605+func (s *StoreSuite) TestLatest(c *C) {
1606+ for _, str := range []string{
1607+ "cs:series/blah",
1608+ "cs:series/blah-2",
1609+ "cs:series/blah-99",
1610+ } {
1611+ r, err := s.store.Latest(charm.MustParseURL(str))
1612+ c.Assert(r, Equals, 23)
1613+ c.Assert(err, IsNil)
1614+ }
1615+}
1616+
1617+func (s *StoreSuite) assertCached(c *C, curl *charm.URL) {
1618+ s.server.downloads = nil
1619+ ch, err := s.store.Get(curl)
1620+ c.Assert(err, IsNil)
1621+ c.Assert(ch, NotNil)
1622+ c.Assert(s.server.downloads, IsNil)
1623+}
1624+
1625+func (s *StoreSuite) TestGetCacheImplicitRevision(c *C) {
1626+ os.RemoveAll(s.cache)
1627+ base := "cs:series/blah"
1628+ curl := charm.MustParseURL(base)
1629+ revCurl := charm.MustParseURL(base + "-23")
1630+ ch, err := s.store.Get(curl)
1631+ c.Assert(err, IsNil)
1632+ c.Assert(ch, NotNil)
1633+ c.Assert(s.server.downloads, DeepEquals, []*charm.URL{revCurl})
1634+ s.assertCached(c, curl)
1635+ s.assertCached(c, revCurl)
1636+}
1637+
1638+func (s *StoreSuite) TestGetCacheExplicitRevision(c *C) {
1639+ os.RemoveAll(s.cache)
1640+ base := "cs:series/blah-12"
1641+ curl := charm.MustParseURL(base)
1642+ ch, err := s.store.Get(curl)
1643+ c.Assert(err, IsNil)
1644+ c.Assert(ch, NotNil)
1645+ c.Assert(s.server.downloads, DeepEquals, []*charm.URL{curl})
1646+ s.assertCached(c, curl)
1647+}
1648+
1649+func (s *StoreSuite) TestGetBadCache(c *C) {
1650+ base := "cs:series/blah"
1651+ curl := charm.MustParseURL(base)
1652+ revCurl := charm.MustParseURL(base + "-23")
1653+ name := charm.Quote(revCurl.String()) + ".charm"
1654+ err := ioutil.WriteFile(filepath.Join(s.cache, name), nil, 0666)
1655+ c.Assert(err, IsNil)
1656+ ch, err := s.store.Get(curl)
1657+ c.Assert(err, IsNil)
1658+ c.Assert(ch, NotNil)
1659+ c.Assert(s.server.downloads, DeepEquals, []*charm.URL{revCurl})
1660+ s.assertCached(c, curl)
1661+ s.assertCached(c, revCurl)
1662+}
1663+
1664+type LocalRepoSuite struct {
1665+ testing.LoggingSuite
1666+ repo *charm.LocalRepository
1667+ seriesPath string
1668+}
1669+
1670+var _ = Suite(&LocalRepoSuite{})
1671+
1672+func (s *LocalRepoSuite) SetUpTest(c *C) {
1673+ s.LoggingSuite.SetUpTest(c)
1674+ root := c.MkDir()
1675+ s.repo = &charm.LocalRepository{root}
1676+ s.seriesPath = filepath.Join(root, "series")
1677+ c.Assert(os.Mkdir(s.seriesPath, 0777), IsNil)
1678+}
1679+
1680+func (s *LocalRepoSuite) addBundle(name string) string {
1681+ return testing.Charms.BundlePath(s.seriesPath, name)
1682+}
1683+
1684+func (s *LocalRepoSuite) addDir(name string) string {
1685+ return testing.Charms.ClonedDirPath(s.seriesPath, name)
1686+}
1687+
1688+func (s *LocalRepoSuite) TestMissingCharm(c *C) {
1689+ _, err := s.repo.Latest(charm.MustParseURL("local:series/zebra"))
1690+ c.Assert(err, ErrorMatches, `no charms found matching "local:series/zebra"`)
1691+ _, err = s.repo.Get(charm.MustParseURL("local:series/zebra"))
1692+ c.Assert(err, ErrorMatches, `no charms found matching "local:series/zebra"`)
1693+ _, err = s.repo.Latest(charm.MustParseURL("local:badseries/zebra"))
1694+ c.Assert(err, ErrorMatches, `no charms found matching "local:badseries/zebra"`)
1695+ _, err = s.repo.Get(charm.MustParseURL("local:badseries/zebra"))
1696+ c.Assert(err, ErrorMatches, `no charms found matching "local:badseries/zebra"`)
1697+}
1698+
1699+func (s *LocalRepoSuite) TestMissingRepo(c *C) {
1700+ c.Assert(os.RemoveAll(s.repo.Path), IsNil)
1701+ _, err := s.repo.Latest(charm.MustParseURL("local:series/zebra"))
1702+ c.Assert(err, ErrorMatches, `no repository found at ".*"`)
1703+ _, err = s.repo.Get(charm.MustParseURL("local:series/zebra"))
1704+ c.Assert(err, ErrorMatches, `no repository found at ".*"`)
1705+ c.Assert(ioutil.WriteFile(s.repo.Path, nil, 0666), IsNil)
1706+ _, err = s.repo.Latest(charm.MustParseURL("local:series/zebra"))
1707+ c.Assert(err, ErrorMatches, `no repository found at ".*"`)
1708+ _, err = s.repo.Get(charm.MustParseURL("local:series/zebra"))
1709+ c.Assert(err, ErrorMatches, `no repository found at ".*"`)
1710+}
1711+
1712+func (s *LocalRepoSuite) TestMultipleVersions(c *C) {
1713+ curl := charm.MustParseURL("local:series/sample")
1714+ s.addDir("old")
1715+ rev, err := s.repo.Latest(curl)
1716+ c.Assert(err, IsNil)
1717+ c.Assert(rev, Equals, 1)
1718+ ch, err := s.repo.Get(curl)
1719+ c.Assert(err, IsNil)
1720+ c.Assert(ch.Revision(), Equals, 1)
1721+
1722+ s.addDir("new")
1723+ rev, err = s.repo.Latest(curl)
1724+ c.Assert(err, IsNil)
1725+ c.Assert(rev, Equals, 2)
1726+ ch, err = s.repo.Get(curl)
1727+ c.Assert(err, IsNil)
1728+ c.Assert(ch.Revision(), Equals, 2)
1729+
1730+ revCurl := curl.WithRevision(1)
1731+ rev, err = s.repo.Latest(revCurl)
1732+ c.Assert(err, IsNil)
1733+ c.Assert(rev, Equals, 2)
1734+ ch, err = s.repo.Get(revCurl)
1735+ c.Assert(err, IsNil)
1736+ c.Assert(ch.Revision(), Equals, 1)
1737+
1738+ badRevCurl := curl.WithRevision(33)
1739+ rev, err = s.repo.Latest(badRevCurl)
1740+ c.Assert(err, IsNil)
1741+ c.Assert(rev, Equals, 2)
1742+ ch, err = s.repo.Get(badRevCurl)
1743+ c.Assert(err, ErrorMatches, `no charms found matching "local:series/sample-33"`)
1744+}
1745+
1746+func (s *LocalRepoSuite) TestBundle(c *C) {
1747+ curl := charm.MustParseURL("local:series/dummy")
1748+ s.addBundle("dummy")
1749+
1750+ rev, err := s.repo.Latest(curl)
1751+ c.Assert(err, IsNil)
1752+ c.Assert(rev, Equals, 1)
1753+ ch, err := s.repo.Get(curl)
1754+ c.Assert(err, IsNil)
1755+ c.Assert(ch.Revision(), Equals, 1)
1756+}
1757+
1758+func (s *LocalRepoSuite) TestLogsErrors(c *C) {
1759+ err := ioutil.WriteFile(filepath.Join(s.seriesPath, "blah.charm"), nil, 0666)
1760+ c.Assert(err, IsNil)
1761+ err = os.Mkdir(filepath.Join(s.seriesPath, "blah"), 0666)
1762+ c.Assert(err, IsNil)
1763+ samplePath := s.addDir("new")
1764+ gibberish := []byte("don't parse me by")
1765+ err = ioutil.WriteFile(filepath.Join(samplePath, "metadata.yaml"), gibberish, 0666)
1766+ c.Assert(err, IsNil)
1767+
1768+ curl := charm.MustParseURL("local:series/dummy")
1769+ s.addDir("dummy")
1770+ ch, err := s.repo.Get(curl)
1771+ c.Assert(err, IsNil)
1772+ c.Assert(ch.Revision(), Equals, 1)
1773+ c.Assert(c.GetTestLog(), Matches, `
1774+.* JUJU WARNING: failed to load charm at ".*/series/blah": .*
1775+.* JUJU WARNING: failed to load charm at ".*/series/blah.charm": .*
1776+.* JUJU WARNING: failed to load charm at ".*/series/new": .*
1777+`[1:])
1778+}
1779+
1780+func renameSibling(c *C, path, name string) {
1781+ c.Assert(os.Rename(path, filepath.Join(filepath.Dir(path), name)), IsNil)
1782+}
1783+
1784+func (s *LocalRepoSuite) TestIgnoresUnpromisingNames(c *C) {
1785+ err := ioutil.WriteFile(filepath.Join(s.seriesPath, "blah.notacharm"), nil, 0666)
1786+ c.Assert(err, IsNil)
1787+ err = os.Mkdir(filepath.Join(s.seriesPath, ".blah"), 0666)
1788+ c.Assert(err, IsNil)
1789+ renameSibling(c, s.addDir("dummy"), ".dummy")
1790+ renameSibling(c, s.addBundle("dummy"), "dummy.notacharm")
1791+ curl := charm.MustParseURL("local:series/dummy")
1792+
1793+ _, err = s.repo.Get(curl)
1794+ c.Assert(err, ErrorMatches, `no charms found matching "local:series/dummy"`)
1795+ _, err = s.repo.Latest(curl)
1796+ c.Assert(err, ErrorMatches, `no charms found matching "local:series/dummy"`)
1797+ c.Assert(c.GetTestLog(), Equals, "")
1798+}
1799
1800=== added file 'charm/url_test.go.OTHER'
1801--- charm/url_test.go.OTHER 1970-01-01 00:00:00 +0000
1802+++ charm/url_test.go.OTHER 2012-06-07 11:57:18 +0000
1803@@ -0,0 +1,126 @@
1804+package charm_test
1805+
1806+import (
1807+ "fmt"
1808+ . "launchpad.net/gocheck"
1809+ "launchpad.net/juju-core/juju/charm"
1810+)
1811+
1812+type URLSuite struct{}
1813+
1814+var _ = Suite(&URLSuite{})
1815+
1816+var urlTests = []struct {
1817+ s, err string
1818+ url *charm.URL
1819+}{
1820+ {"cs:~user/series/name", "", &charm.URL{"cs", "user", "series", "name", -1}},
1821+ {"cs:~user/series/name-0", "", &charm.URL{"cs", "user", "series", "name", 0}},
1822+ {"cs:series/name", "", &charm.URL{"cs", "", "series", "name", -1}},
1823+ {"cs:series/name-42", "", &charm.URL{"cs", "", "series", "name", 42}},
1824+ {"local:series/name-1", "", &charm.URL{"local", "", "series", "name", 1}},
1825+ {"local:series/name", "", &charm.URL{"local", "", "series", "name", -1}},
1826+ {"local:series/n0-0n-n0", "", &charm.URL{"local", "", "series", "n0-0n-n0", -1}},
1827+
1828+ {"bs:~user/series/name-1", "charm URL has invalid schema: .*", nil},
1829+ {"cs:~1/series/name-1", "charm URL has invalid user name: .*", nil},
1830+ {"cs:~user/1/name-1", "charm URL has invalid series: .*", nil},
1831+ {"cs:~user/series/name-1-2", "charm URL has invalid charm name: .*", nil},
1832+ {"cs:~user/series/name-1-name-2", "charm URL has invalid charm name: .*", nil},
1833+ {"cs:~user/series/name--name-2", "charm URL has invalid charm name: .*", nil},
1834+ {"cs:~user/series/huh/name-1", "charm URL has invalid form: .*", nil},
1835+ {"cs:~user/name", "charm URL without series: .*", nil},
1836+ {"cs:name", "charm URL without series: .*", nil},
1837+ {"local:~user/series/name", "local charm URL with user name: .*", nil},
1838+ {"local:~user/name", "local charm URL with user name: .*", nil},
1839+ {"local:name", "charm URL without series: .*", nil},
1840+}
1841+
1842+func (s *URLSuite) TestParseURL(c *C) {
1843+ for _, t := range urlTests {
1844+ url, err := charm.ParseURL(t.s)
1845+ comment := Commentf("ParseURL(%q)", t.s)
1846+ if t.err != "" {
1847+ c.Check(err.Error(), Matches, t.err, comment)
1848+ } else {
1849+ c.Check(url, DeepEquals, t.url, comment)
1850+ c.Check(t.url.String(), Equals, t.s)
1851+ }
1852+ }
1853+}
1854+
1855+var inferTests = []struct {
1856+ vague, exact string
1857+}{
1858+ {"foo", "cs:defseries/foo"},
1859+ {"foo-1", "cs:defseries/foo-1"},
1860+ {"n0-n0-n0", "cs:defseries/n0-n0-n0"},
1861+ {"cs:foo", "cs:defseries/foo"},
1862+ {"local:foo", "local:defseries/foo"},
1863+ {"series/foo", "cs:series/foo"},
1864+ {"cs:series/foo", "cs:series/foo"},
1865+ {"local:series/foo", "local:series/foo"},
1866+ {"cs:~user/foo", "cs:~user/defseries/foo"},
1867+ {"cs:~user/series/foo", "cs:~user/series/foo"},
1868+ {"local:~user/series/foo", "local:~user/series/foo"},
1869+ {"bs:foo", "bs:defseries/foo"},
1870+ {"cs:~1/foo", "cs:~1/defseries/foo"},
1871+ {"cs:foo-1-2", "cs:defseries/foo-1-2"},
1872+}
1873+
1874+func (s *URLSuite) TestInferURL(c *C) {
1875+ for _, t := range inferTests {
1876+ comment := Commentf("InferURL(%q, %q)", t.vague, "defseries")
1877+ inferred, ierr := charm.InferURL(t.vague, "defseries")
1878+ parsed, perr := charm.ParseURL(t.exact)
1879+ if parsed != nil {
1880+ c.Check(inferred, DeepEquals, parsed, comment)
1881+ } else {
1882+ expect := perr.Error()
1883+ if t.vague != t.exact {
1884+ expect = fmt.Sprintf("%s (URL inferred from %q)", expect, t.vague)
1885+ }
1886+ c.Check(ierr.Error(), Equals, expect, comment)
1887+ }
1888+ }
1889+ u, err := charm.InferURL("~blah", "defseries")
1890+ c.Assert(u, IsNil)
1891+ c.Assert(err, ErrorMatches, "cannot infer charm URL with user but no schema: .*")
1892+}
1893+
1894+func (s *URLSuite) TestMustParseURL(c *C) {
1895+ url := charm.MustParseURL("cs:series/name")
1896+ c.Assert(url, DeepEquals, &charm.URL{"cs", "", "series", "name", -1})
1897+ f := func() { charm.MustParseURL("local:name") }
1898+ c.Assert(f, PanicMatches, "charm URL without series: .*")
1899+}
1900+
1901+func (s *URLSuite) TestWithRevision(c *C) {
1902+ url := charm.MustParseURL("cs:series/name")
1903+ other := url.WithRevision(1)
1904+ c.Assert(url, DeepEquals, &charm.URL{"cs", "", "series", "name", -1})
1905+ c.Assert(other, DeepEquals, &charm.URL{"cs", "", "series", "name", 1})
1906+
1907+ // Should always copy. The opposite behavior is error prone.
1908+ c.Assert(other.WithRevision(1), Not(Equals), other)
1909+ c.Assert(other.WithRevision(1), DeepEquals, other)
1910+}
1911+
1912+type QuoteSuite struct{}
1913+
1914+var _ = Suite(&QuoteSuite{})
1915+
1916+func (s *QuoteSuite) TestUnmodified(c *C) {
1917+ // Check that a string containing only valid
1918+ // chars stays unmodified.
1919+ in := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-"
1920+ out := charm.Quote(in)
1921+ c.Assert(out, Equals, in)
1922+}
1923+
1924+func (s *QuoteSuite) TestQuote(c *C) {
1925+ // Check that invalid chars are translated correctly.
1926+ in := "hello_there/how'are~you-today.sir"
1927+ out := charm.Quote(in)
1928+ c.Assert(out, Equals, "hello_5f_there_2f_how_27_are_7e_you-today.sir")
1929+}
1930
1931=== added directory 'cloudinit'
1932=== added file 'cloudinit/cloudinit_test.go.OTHER'
1933--- cloudinit/cloudinit_test.go.OTHER 1970-01-01 00:00:00 +0000
1934+++ cloudinit/cloudinit_test.go.OTHER 2012-06-07 11:57:18 +0000
1935@@ -0,0 +1,228 @@
1936+package cloudinit_test
1937+
1938+import (
1939+ "fmt"
1940+ . "launchpad.net/gocheck"
1941+ "launchpad.net/juju-core/juju/cloudinit"
1942+ "testing"
1943+)
1944+
1945+// TODO integration tests, but how?
1946+
1947+type S struct{}
1948+
1949+var _ = Suite(S{})
1950+
1951+func Test1(t *testing.T) {
1952+ TestingT(t)
1953+}
1954+
1955+var ctests = []struct {
1956+ name string
1957+ expect string
1958+ setOption func(cfg *cloudinit.Config)
1959+}{
1960+ {
1961+ "User",
1962+ "user: me\n",
1963+ func(cfg *cloudinit.Config) {
1964+ cfg.SetUser("me")
1965+ },
1966+ },
1967+ {
1968+ "AptUpgrade",
1969+ "apt_upgrade: true\n",
1970+ func(cfg *cloudinit.Config) {
1971+ cfg.SetAptUpgrade(true)
1972+ },
1973+ },
1974+ {
1975+ "AptUpdate",
1976+ "apt_update: true\n",
1977+ func(cfg *cloudinit.Config) {
1978+ cfg.SetAptUpdate(true)
1979+ },
1980+ },
1981+ {
1982+ "AptMirror",
1983+ "apt_mirror: http://foo.com\n",
1984+ func(cfg *cloudinit.Config) {
1985+ cfg.SetAptMirror("http://foo.com")
1986+ },
1987+ },
1988+ {
1989+ "AptPreserveSourcesList",
1990+ "apt_mirror: true\n",
1991+ func(cfg *cloudinit.Config) {
1992+ cfg.SetAptPreserveSourcesList(true)
1993+ },
1994+ },
1995+ {
1996+ "DebconfSelections",
1997+ "debconf_selections: '# Force debconf priority to critical.\n\n debconf debconf/priority select critical\n\n'\n",
1998+ func(cfg *cloudinit.Config) {
1999+ cfg.SetDebconfSelections("# Force debconf priority to critical.\ndebconf debconf/priority select critical\n")
2000+ },
2001+ },
2002+ {
2003+ "DisableEC2Metadata",
2004+ "disable_ec2_metadata: true\n",
2005+ func(cfg *cloudinit.Config) {
2006+ cfg.SetDisableEC2Metadata(true)
2007+ },
2008+ },
2009+ {
2010+ "FinalMessage",
2011+ "final_message: goodbye\n",
2012+ func(cfg *cloudinit.Config) {
2013+ cfg.SetFinalMessage("goodbye")
2014+ },
2015+ },
2016+ {
2017+ "Locale",
2018+ "locale: en_us\n",
2019+ func(cfg *cloudinit.Config) {
2020+ cfg.SetLocale("en_us")
2021+ },
2022+ },
2023+ {
2024+ "DisableRoot",
2025+ "disable_root: false\n",
2026+ func(cfg *cloudinit.Config) {
2027+ cfg.SetDisableRoot(false)
2028+ },
2029+ },
2030+ {
2031+ "SSHAuthorizedKeys",
2032+ "ssh_authorized_keys:\n- key1\n- key2\n",
2033+ func(cfg *cloudinit.Config) {
2034+ cfg.AddSSHAuthorizedKeys("key1")
2035+ cfg.AddSSHAuthorizedKeys("key2")
2036+ },
2037+ },
2038+ {
2039+ "SSHAuthorizedKeys",
2040+ "ssh_authorized_keys:\n- key1\n- key2\n- key3\n",
2041+ func(cfg *cloudinit.Config) {
2042+ cfg.AddSSHAuthorizedKeys("#command\nkey1")
2043+ cfg.AddSSHAuthorizedKeys("key2\n# comment\n\nkey3\n")
2044+ cfg.AddSSHAuthorizedKeys("")
2045+ },
2046+ },
2047+ {
2048+ "SSHKeys RSAPrivate",
2049+ "ssh_keys:\n rsa_private: key1data\n",
2050+ func(cfg *cloudinit.Config) {
2051+ cfg.AddSSHKey(cloudinit.RSAPrivate, "key1data")
2052+ },
2053+ },
2054+ {
2055+ "SSHKeys RSAPublic",
2056+ "ssh_keys:\n rsa_public: key2data\n",
2057+ func(cfg *cloudinit.Config) {
2058+ cfg.AddSSHKey(cloudinit.RSAPublic, "key2data")
2059+ },
2060+ },
2061+ {
2062+ "SSHKeys DSAPublic",
2063+ "ssh_keys:\n dsa_public: key1data\n",
2064+ func(cfg *cloudinit.Config) {
2065+ cfg.AddSSHKey(cloudinit.DSAPublic, "key1data")
2066+ },
2067+ },
2068+ {
2069+ "SSHKeys DSAPrivate",
2070+ "ssh_keys:\n dsa_private: key2data\n",
2071+ func(cfg *cloudinit.Config) {
2072+ cfg.AddSSHKey(cloudinit.DSAPrivate, "key2data")
2073+ },
2074+ },
2075+ {
2076+ "Output",
2077+ "output:\n all:\n - '>foo'\n - '|bar'\n",
2078+ func(cfg *cloudinit.Config) {
2079+ cfg.SetOutput("all", ">foo", "|bar")
2080+ },
2081+ },
2082+ {
2083+ "Output",
2084+ "output:\n all: '>foo'\n",
2085+ func(cfg *cloudinit.Config) {
2086+ cfg.SetOutput(cloudinit.OutAll, ">foo", "")
2087+ },
2088+ },
2089+ {
2090+ "AptSources",
2091+ "apt_sources:\n- source: keyName\n key: someKey\n",
2092+ func(cfg *cloudinit.Config) {
2093+ cfg.AddAptSource("keyName", "someKey")
2094+ },
2095+ },
2096+ {
2097+ "AptSources",
2098+ "apt_sources:\n- source: keyName\n keyid: someKey\n keyserver: foo.com\n",
2099+ func(cfg *cloudinit.Config) {
2100+ cfg.AddAptSourceWithKeyId("keyName", "someKey", "foo.com")
2101+ },
2102+ },
2103+ {
2104+ "Packages",
2105+ "packages:\n- juju\n- ubuntu\n",
2106+ func(cfg *cloudinit.Config) {
2107+ cfg.AddPackage("juju")
2108+ cfg.AddPackage("ubuntu")
2109+ },
2110+ },
2111+ {
2112+ "BootCmd",
2113+ "bootcmd:\n- ls > /dev\n- - ls\n - '>with space'\n",
2114+ func(cfg *cloudinit.Config) {
2115+ cfg.AddBootCmd("ls > /dev")
2116+ cfg.AddBootCmdArgs("ls", ">with space")
2117+ },
2118+ },
2119+ {
2120+ "Mounts",
2121+ "mounts:\n- - x\n - \"y\"\n- - z\n - w\n",
2122+ func(cfg *cloudinit.Config) {
2123+ cfg.AddMount("x", "y")
2124+ cfg.AddMount("z", "w")
2125+ },
2126+ },
2127+ {
2128+ "Attr",
2129+ "arbitraryAttr: someValue\n",
2130+ func(cfg *cloudinit.Config) {
2131+ cfg.SetAttr("arbitraryAttr", "someValue")
2132+ },
2133+ },
2134+}
2135+
2136+const header = "#cloud-config\n"
2137+
2138+func (S) TestOutput(c *C) {
2139+ for _, t := range ctests {
2140+ cfg := cloudinit.New()
2141+ t.setOption(cfg)
2142+ data, err := cfg.Render()
2143+ c.Assert(err, IsNil)
2144+ c.Assert(data, NotNil)
2145+ c.Assert(string(data), Equals, header+t.expect, Commentf("test %q output differs", t.name))
2146+ }
2147+}
2148+
2149+//#cloud-config
2150+//packages:
2151+//- juju
2152+//- ubuntu
2153+func ExampleConfig() {
2154+ cfg := cloudinit.New()
2155+ cfg.AddPackage("juju")
2156+ cfg.AddPackage("ubuntu")
2157+ data, err := cfg.Render()
2158+ if err != nil {
2159+ fmt.Printf("render error: %v", err)
2160+ return
2161+ }
2162+ fmt.Printf("%s", data)
2163+}
2164
2165=== added directory 'cmd'
2166=== added file 'cmd/cmd.go.OTHER'
2167--- cmd/cmd.go.OTHER 1970-01-01 00:00:00 +0000
2168+++ cmd/cmd.go.OTHER 2012-06-07 11:57:18 +0000
2169@@ -0,0 +1,139 @@
2170+package cmd
2171+
2172+import (
2173+ "bytes"
2174+ "errors"
2175+ "fmt"
2176+ "io"
2177+ "io/ioutil"
2178+ "launchpad.net/gnuflag"
2179+ "launchpad.net/juju-core/juju/log"
2180+ "os"
2181+ "path/filepath"
2182+ "strings"
2183+)
2184+
2185+// ErrSilent can be returned from Run to signal that Main should exit with
2186+// code 1 without producing error output.
2187+var ErrSilent = errors.New("cmd: error out silently")
2188+
2189+// Command is implemented by types that interpret command-line arguments.
2190+type Command interface {
2191+ // Info returns information about the Command.
2192+ Info() *Info
2193+
2194+ // Init initializes the Command before running. The command may add options
2195+ // to f before processing args.
2196+ Init(f *gnuflag.FlagSet, args []string) error
2197+
2198+ // Run will execute the Command as directed by the options and positional
2199+ // arguments passed to Init.
2200+ Run(ctx *Context) error
2201+}
2202+
2203+// Context represents the run context of a Command. Command implementations
2204+// should interpret file names relative to Dir (see AbsPath below), and print
2205+// output and errors to Stdout and Stderr respectively.
2206+type Context struct {
2207+ Dir string
2208+ Stdout io.Writer
2209+ Stderr io.Writer
2210+}
2211+
2212+// AbsPath returns an absolute representation of path, with relative paths
2213+// interpreted as relative to ctx.Dir.
2214+func (ctx *Context) AbsPath(path string) string {
2215+ if filepath.IsAbs(path) {
2216+ return path
2217+ }
2218+ return filepath.Join(ctx.Dir, path)
2219+}
2220+
2221+// Info holds some of the usage documentation of a Command.
2222+type Info struct {
2223+ // Name is the Command's name.
2224+ Name string
2225+
2226+ // Args describes the command's expected positional arguments.
2227+ Args string
2228+
2229+ // Purpose is a short explanation of the Command's purpose.
2230+ Purpose string
2231+
2232+ // Doc is the long documentation for the Command.
2233+ Doc string
2234+}
2235+
2236+// Help renders i's content, along with documentation for any
2237+// flags defined in f. It calls f.SetOutput(ioutil.Discard).
2238+func (i *Info) Help(f *gnuflag.FlagSet) []byte {
2239+ buf := &bytes.Buffer{}
2240+ fmt.Fprintf(buf, "usage: %s", i.Name)
2241+ hasOptions := false
2242+ f.VisitAll(func(f *gnuflag.Flag) { hasOptions = true })
2243+ if hasOptions {
2244+ fmt.Fprintf(buf, " [options]")
2245+ }
2246+ if i.Args != "" {
2247+ fmt.Fprintf(buf, " %s", i.Args)
2248+ }
2249+ fmt.Fprintf(buf, "\n")
2250+ if i.Purpose != "" {
2251+ fmt.Fprintf(buf, "purpose: %s\n", i.Purpose)
2252+ }
2253+ if hasOptions {
2254+ fmt.Fprintf(buf, "\noptions:\n")
2255+ f.SetOutput(buf)
2256+ f.PrintDefaults()
2257+ }
2258+ f.SetOutput(ioutil.Discard)
2259+ if i.Doc != "" {
2260+ fmt.Fprintf(buf, "\n%s\n", strings.TrimSpace(i.Doc))
2261+ }
2262+ return buf.Bytes()
2263+}
2264+
2265+// Main runs the given Command in the supplied Context with the given
2266+// arguments, which should not include the command name. It returns a code
2267+// suitable for passing to os.Exit.
2268+func Main(c Command, ctx *Context, args []string) int {
2269+ f := gnuflag.NewFlagSet(c.Info().Name, gnuflag.ContinueOnError)
2270+ f.SetOutput(ioutil.Discard)
2271+ if err := c.Init(f, args); err != nil {
2272+ ctx.Stderr.Write(c.Info().Help(f))
2273+ if err == gnuflag.ErrHelp {
2274+ return 0
2275+ }
2276+ fmt.Fprintf(ctx.Stderr, "error: %v\n", err)
2277+ return 2
2278+ }
2279+ if err := c.Run(ctx); err != nil {
2280+ if err != ErrSilent {
2281+ log.Debugf("%s command failed: %s\n", c.Info().Name, err)
2282+ fmt.Fprintf(ctx.Stderr, "error: %v\n", err)
2283+ }
2284+ return 1
2285+ }
2286+ return 0
2287+}
2288+
2289+// DefaultContext returns a Context suitable for use in non-hosted situations.
2290+func DefaultContext() *Context {
2291+ dir, err := os.Getwd()
2292+ if err != nil {
2293+ panic(err)
2294+ }
2295+ abs, err := filepath.Abs(dir)
2296+ if err != nil {
2297+ panic(err)
2298+ }
2299+ return &Context{abs, os.Stdout, os.Stderr}
2300+}
2301+
2302+// CheckEmpty is a utility function that returns an error if args is not empty.
2303+func CheckEmpty(args []string) error {
2304+ if len(args) != 0 {
2305+ return fmt.Errorf("unrecognized args: %q", args)
2306+ }
2307+ return nil
2308+}
2309
2310=== added file 'cmd/cmd_test.go.OTHER'
2311--- cmd/cmd_test.go.OTHER 1970-01-01 00:00:00 +0000
2312+++ cmd/cmd_test.go.OTHER 2012-06-07 11:57:18 +0000
2313@@ -0,0 +1,96 @@
2314+package cmd_test
2315+
2316+import (
2317+ . "launchpad.net/gocheck"
2318+ "launchpad.net/juju-core/juju/cmd"
2319+ "path/filepath"
2320+ "testing"
2321+)
2322+
2323+func Test(t *testing.T) { TestingT(t) }
2324+
2325+type CmdSuite struct{}
2326+
2327+var _ = Suite(&CmdSuite{})
2328+
2329+func (s *CmdSuite) TestContext(c *C) {
2330+ ctx := dummyContext(c)
2331+ c.Assert(ctx.AbsPath("/foo/bar"), Equals, "/foo/bar")
2332+ c.Assert(ctx.AbsPath("foo/bar"), Equals, filepath.Join(ctx.Dir, "foo/bar"))
2333+}
2334+
2335+func (s *CmdSuite) TestInfo(c *C) {
2336+ minimal := &TestCommand{Name: "verb", Minimal: true}
2337+ help := minimal.Info().Help(dummyFlagSet())
2338+ c.Assert(string(help), Equals, minimalHelp)
2339+
2340+ full := &TestCommand{Name: "verb"}
2341+ f := dummyFlagSet()
2342+ var ignored string
2343+ f.StringVar(&ignored, "option", "", "option-doc")
2344+ help = full.Info().Help(f)
2345+ c.Assert(string(help), Equals, fullHelp)
2346+
2347+ optionInfo := full.Info()
2348+ optionInfo.Doc = ""
2349+ help = optionInfo.Help(f)
2350+ c.Assert(string(help), Equals, optionHelp)
2351+}
2352+
2353+var initErrorTests = []struct {
2354+ c *TestCommand
2355+ help string
2356+}{
2357+ {&TestCommand{Name: "verb"}, fullHelp},
2358+ {&TestCommand{Name: "verb", Minimal: true}, minimalHelp},
2359+}
2360+
2361+func (s *CmdSuite) TestMainInitError(c *C) {
2362+ for _, t := range initErrorTests {
2363+ ctx := dummyContext(c)
2364+ result := cmd.Main(t.c, ctx, []string{"--unknown"})
2365+ c.Assert(result, Equals, 2)
2366+ c.Assert(bufferString(ctx.Stdout), Equals, "")
2367+ expected := t.help + "error: flag provided but not defined: --unknown\n"
2368+ c.Assert(bufferString(ctx.Stderr), Equals, expected)
2369+ }
2370+}
2371+
2372+func (s *CmdSuite) TestMainRunError(c *C) {
2373+ ctx := dummyContext(c)
2374+ result := cmd.Main(&TestCommand{Name: "verb"}, ctx, []string{"--option", "error"})
2375+ c.Assert(result, Equals, 1)
2376+ c.Assert(bufferString(ctx.Stdout), Equals, "")
2377+ c.Assert(bufferString(ctx.Stderr), Equals, "error: BAM!\n")
2378+}
2379+
2380+func (s *CmdSuite) TestMainRunSilentError(c *C) {
2381+ ctx := dummyContext(c)
2382+ result := cmd.Main(&TestCommand{Name: "verb"}, ctx, []string{"--option", "silent-error"})
2383+ c.Assert(result, Equals, 1)
2384+ c.Assert(bufferString(ctx.Stdout), Equals, "")
2385+ c.Assert(bufferString(ctx.Stderr), Equals, "")
2386+}
2387+
2388+func (s *CmdSuite) TestMainSuccess(c *C) {
2389+ ctx := dummyContext(c)
2390+ result := cmd.Main(&TestCommand{Name: "verb"}, ctx, []string{"--option", "success!"})
2391+ c.Assert(result, Equals, 0)
2392+ c.Assert(bufferString(ctx.Stdout), Equals, "success!\n")
2393+ c.Assert(bufferString(ctx.Stderr), Equals, "")
2394+}
2395+
2396+func (s *CmdSuite) TestMainHelp(c *C) {
2397+ for _, arg := range []string{"-h", "--help"} {
2398+ ctx := dummyContext(c)
2399+ result := cmd.Main(&TestCommand{Name: "verb"}, ctx, []string{arg})
2400+ c.Assert(result, Equals, 0)
2401+ c.Assert(bufferString(ctx.Stdout), Equals, "")
2402+ c.Assert(bufferString(ctx.Stderr), Equals, fullHelp)
2403+ }
2404+}
2405+
2406+func (s *CmdSuite) TestCheckEmpty(c *C) {
2407+ c.Assert(cmd.CheckEmpty(nil), IsNil)
2408+ c.Assert(cmd.CheckEmpty([]string{"boo!"}), ErrorMatches, `unrecognized args: \["boo!"\]`)
2409+}
2410
2411=== added directory 'cmd/juju'
2412=== added file 'cmd/juju/bootstrap.go.OTHER'
2413--- cmd/juju/bootstrap.go.OTHER 1970-01-01 00:00:00 +0000
2414+++ cmd/juju/bootstrap.go.OTHER 2012-06-07 11:57:18 +0000
2415@@ -0,0 +1,37 @@
2416+package main
2417+
2418+import (
2419+ "launchpad.net/gnuflag"
2420+ "launchpad.net/juju-core/juju/cmd"
2421+ "launchpad.net/juju-core/juju/juju"
2422+)
2423+
2424+// BootstrapCommand is responsible for launching the first machine in a juju
2425+// environment, and setting up everything necessary to continue working.
2426+type BootstrapCommand struct {
2427+ EnvName string
2428+ UploadTools bool
2429+}
2430+
2431+func (c *BootstrapCommand) Info() *cmd.Info {
2432+ return &cmd.Info{"bootstrap", "", "start up an environment from scratch", ""}
2433+}
2434+
2435+func (c *BootstrapCommand) Init(f *gnuflag.FlagSet, args []string) error {
2436+ addEnvironFlags(&c.EnvName, f)
2437+ f.BoolVar(&c.UploadTools, "upload-tools", false, "upload local version of tools before bootstrapping")
2438+ if err := f.Parse(true, args); err != nil {
2439+ return err
2440+ }
2441+ return cmd.CheckEmpty(f.Args())
2442+}
2443+
2444+// Run connects to the environment specified on the command line and bootstraps
2445+// a juju in that environment if none already exists.
2446+func (c *BootstrapCommand) Run(_ *cmd.Context) error {
2447+ conn, err := juju.NewConn(c.EnvName)
2448+ if err != nil {
2449+ return err
2450+ }
2451+ return conn.Bootstrap(c.UploadTools)
2452+}
2453
2454=== added file 'cmd/juju/cmd_test.go.OTHER'
2455--- cmd/juju/cmd_test.go.OTHER 1970-01-01 00:00:00 +0000
2456+++ cmd/juju/cmd_test.go.OTHER 2012-06-07 11:57:18 +0000
2457@@ -0,0 +1,255 @@
2458+package main
2459+
2460+import (
2461+ "io/ioutil"
2462+ "launchpad.net/gnuflag"
2463+ . "launchpad.net/gocheck"
2464+ "launchpad.net/juju-core/juju/cmd"
2465+ "launchpad.net/juju-core/juju/environs"
2466+ "launchpad.net/juju-core/juju/environs/dummy"
2467+ "launchpad.net/juju-core/juju/testing"
2468+ "os"
2469+ "path/filepath"
2470+ "reflect"
2471+)
2472+
2473+type cmdSuite struct {
2474+ testing.LoggingSuite
2475+ home string
2476+}
2477+
2478+var _ = Suite(&cmdSuite{})
2479+
2480+// N.B. Barking is broken.
2481+var config = `
2482+default:
2483+ peckham
2484+environments:
2485+ peckham:
2486+ type: dummy
2487+ zookeeper: false
2488+ walthamstow:
2489+ type: dummy
2490+ zookeeper: false
2491+ barking:
2492+ type: dummy
2493+ broken: true
2494+ zookeeper: false
2495+`
2496+
2497+func (s *cmdSuite) SetUpTest(c *C) {
2498+ s.LoggingSuite.SetUpTest(c)
2499+ // Arrange so that the "home" directory points
2500+ // to a temporary directory containing the config file.
2501+ s.home = os.Getenv("HOME")
2502+ dir := c.MkDir()
2503+ os.Setenv("HOME", dir)
2504+ err := os.Mkdir(filepath.Join(dir, ".juju"), 0777)
2505+ c.Assert(err, IsNil)
2506+ err = ioutil.WriteFile(filepath.Join(dir, ".juju", "environments.yaml"), []byte(config), 0666)
2507+ c.Assert(err, IsNil)
2508+}
2509+
2510+func (s *cmdSuite) TearDownTest(c *C) {
2511+ os.Setenv("HOME", s.home)
2512+
2513+ dummy.Reset()
2514+ s.LoggingSuite.TearDownTest(c)
2515+}
2516+
2517+func newFlagSet() *gnuflag.FlagSet {
2518+ return gnuflag.NewFlagSet("", gnuflag.ContinueOnError)
2519+}
2520+
2521+// testInit checks that a command initialises correctly
2522+// with the given set of arguments.
2523+func testInit(c *C, com cmd.Command, args []string, errPat string) {
2524+ err := com.Init(newFlagSet(), args)
2525+ if errPat != "" {
2526+ c.Assert(err, ErrorMatches, errPat)
2527+ } else {
2528+ c.Assert(err, IsNil)
2529+ }
2530+}
2531+
2532+// assertConnName asserts that the Command is using
2533+// the given environment name.
2534+// Since every command has a different type,
2535+// we use reflection to look at the value of the
2536+// Conn field in the value.
2537+func assertConnName(c *C, com cmd.Command, name string) {
2538+ v := reflect.ValueOf(com).Elem().FieldByName("EnvName")
2539+ c.Assert(v.IsValid(), Equals, true)
2540+ c.Assert(v.Interface(), Equals, name)
2541+}
2542+
2543+// All members of EnvironmentInitTests are tested for the -environment and -e
2544+// flags, and that extra arguments will cause parsing to fail.
2545+var EnvironmentInitTests = []func() (cmd.Command, []string){
2546+ func() (cmd.Command, []string) { return new(BootstrapCommand), nil },
2547+ func() (cmd.Command, []string) { return new(DestroyCommand), nil },
2548+ func() (cmd.Command, []string) {
2549+ return new(DeployCommand), []string{"charm-name", "service-name"}
2550+ },
2551+}
2552+
2553+// TestEnvironmentInit tests that all commands which accept
2554+// the --environment variable initialise their
2555+// environment name correctly.
2556+func (*cmdSuite) TestEnvironmentInit(c *C) {
2557+ for i, cmdFunc := range EnvironmentInitTests {
2558+ c.Logf("test %d", i)
2559+ com, args := cmdFunc()
2560+ testInit(c, com, args, "")
2561+ assertConnName(c, com, "")
2562+
2563+ com, args = cmdFunc()
2564+ testInit(c, com, append(args, "-e", "walthamstow"), "")
2565+ assertConnName(c, com, "walthamstow")
2566+
2567+ com, args = cmdFunc()
2568+ testInit(c, com, append(args, "--environment", "walthamstow"), "")
2569+ assertConnName(c, com, "walthamstow")
2570+
2571+ com, args = cmdFunc()
2572+ testInit(c, com, append(args, "hotdog"), "unrecognized args.*")
2573+ }
2574+}
2575+
2576+func runCommand(com cmd.Command, args ...string) (opc chan dummy.Operation, errc chan error) {
2577+ errc = make(chan error, 1)
2578+ opc = make(chan dummy.Operation)
2579+ dummy.Reset()
2580+ dummy.Listen(opc)
2581+ go func() {
2582+ // signal that we're done with this ops channel.
2583+ defer dummy.Listen(nil)
2584+
2585+ err := com.Init(newFlagSet(), args)
2586+ if err != nil {
2587+ errc <- err
2588+ return
2589+ }
2590+
2591+ err = com.Run(cmd.DefaultContext())
2592+ errc <- err
2593+ }()
2594+ return
2595+}
2596+
2597+func op(kind dummy.OperationKind, name string) dummy.Operation {
2598+ return dummy.Operation{
2599+ Env: name,
2600+ Kind: kind,
2601+ }
2602+}
2603+
2604+func (*cmdSuite) TestBootstrapCommand(c *C) {
2605+ // normal bootstrap
2606+ opc, errc := runCommand(new(BootstrapCommand))
2607+ c.Check(<-opc, Equals, op(dummy.OpBootstrap, "peckham"))
2608+ c.Check(<-errc, IsNil)
2609+
2610+ // bootstrap with tool uploading - checking that a file
2611+ // is uploaded should be sufficient, as the detailed semantics
2612+ // of UploadTools are tested in environs.
2613+ opc, errc = runCommand(new(BootstrapCommand), "--upload-tools")
2614+ c.Check(<-opc, Equals, op(dummy.OpPutFile, "peckham"))
2615+ c.Check(<-opc, Equals, op(dummy.OpBootstrap, "peckham"))
2616+ c.Check(<-errc, IsNil)
2617+
2618+ envs, err := environs.ReadEnvirons("")
2619+ c.Assert(err, IsNil)
2620+ env, err := envs.Open("peckham")
2621+ c.Assert(err, IsNil)
2622+ dir := c.MkDir()
2623+ err = environs.GetTools(env, dir)
2624+ c.Assert(err, IsNil)
2625+
2626+ // bootstrap with broken environment
2627+ opc, errc = runCommand(new(BootstrapCommand), "-e", "barking")
2628+ c.Check((<-opc).Kind, Equals, dummy.OpNone)
2629+ c.Check(<-errc, ErrorMatches, `broken environment`)
2630+}
2631+
2632+func (*cmdSuite) TestDestroyCommand(c *C) {
2633+ // normal destroy
2634+ opc, errc := runCommand(new(DestroyCommand))
2635+ c.Check(<-opc, Equals, op(dummy.OpDestroy, "peckham"))
2636+ c.Check(<-errc, IsNil)
2637+
2638+ // destroy with broken environment
2639+ opc, errc = runCommand(new(DestroyCommand), "-e", "barking")
2640+ c.Check((<-opc).Kind, Equals, dummy.OpNone)
2641+ c.Check(<-errc, ErrorMatches, `broken environment`)
2642+}
2643+
2644+var deployTests = []struct {
2645+ args []string
2646+ com *DeployCommand
2647+}{
2648+ {
2649+ []string{"charm-name"},
2650+ &DeployCommand{},
2651+ }, {
2652+ []string{"charm-name", "service-name"},
2653+ &DeployCommand{ServiceName: "service-name"},
2654+ }, {
2655+ []string{"--config", "/path/to/config.yaml", "charm-name"},
2656+ &DeployCommand{ConfPath: "/path/to/config.yaml"},
2657+ }, {
2658+ []string{"--repository", "/path/to/another-repo", "charm-name"},
2659+ &DeployCommand{RepoPath: "/path/to/another-repo"},
2660+ }, {
2661+ []string{"--upgrade", "charm-name"},
2662+ &DeployCommand{Upgrade: true},
2663+ }, {
2664+ []string{"-u", "charm-name"},
2665+ &DeployCommand{Upgrade: true},
2666+ }, {
2667+ []string{"--num-units", "33", "charm-name"},
2668+ &DeployCommand{NumUnits: 33},
2669+ }, {
2670+ []string{"-n", "104", "charm-name"},
2671+ &DeployCommand{NumUnits: 104},
2672+ },
2673+}
2674+
2675+func initExpectations(com *DeployCommand) {
2676+ if com.CharmName == "" {
2677+ com.CharmName = "charm-name"
2678+ }
2679+ if com.NumUnits == 0 {
2680+ com.NumUnits = 1
2681+ }
2682+ if com.RepoPath == "" {
2683+ com.RepoPath = "/path/to/repo"
2684+ }
2685+}
2686+
2687+func initDeployCommand(args ...string) (*DeployCommand, error) {
2688+ com := &DeployCommand{}
2689+ return com, com.Init(newFlagSet(), args)
2690+}
2691+
2692+func (*cmdSuite) TestDeployCommandInit(c *C) {
2693+ defer os.Setenv("JUJU_REPOSITORY", os.Getenv("JUJU_REPOSITORY"))
2694+ os.Setenv("JUJU_REPOSITORY", "/path/to/repo")
2695+
2696+ for _, t := range deployTests {
2697+ initExpectations(t.com)
2698+ com, err := initDeployCommand(t.args...)
2699+ c.Assert(err, IsNil)
2700+ c.Assert(com, DeepEquals, t.com)
2701+ }
2702+
2703+ // missing args
2704+ _, err := initDeployCommand()
2705+ c.Assert(err, ErrorMatches, "no charm specified")
2706+
2707+ // bad unit count
2708+ _, err = initDeployCommand("charm-name", "--num-units", "0")
2709+ c.Assert(err, ErrorMatches, "must deploy at least one unit")
2710+
2711+ // environment tested elsewhere
2712+}
2713
2714=== added file 'cmd/juju/deploy.go.OTHER'
2715--- cmd/juju/deploy.go.OTHER 1970-01-01 00:00:00 +0000
2716+++ cmd/juju/deploy.go.OTHER 2012-06-07 11:57:18 +0000
2717@@ -0,0 +1,79 @@
2718+package main
2719+
2720+import (
2721+ "errors"
2722+ "launchpad.net/gnuflag"
2723+ "launchpad.net/juju-core/juju/cmd"
2724+ "os"
2725+)
2726+
2727+type DeployCommand struct {
2728+ EnvName string
2729+ CharmName string
2730+ ServiceName string
2731+ ConfPath string
2732+ NumUnits int
2733+ Upgrade bool
2734+ RepoPath string // defaults to JUJU_REPOSITORY
2735+}
2736+
2737+const deployDoc = `
2738+<charm name> can be a charm URL, or an unambiguously condensed form of it;
2739+assuming a current default series of "precise", the following forms will be
2740+accepted.
2741+
2742+For cs:precise/mysql
2743+ mysql
2744+ precise/mysql
2745+
2746+For cs:~user/precise/mysql
2747+ cs:~user/mysql
2748+
2749+For local:precise/mysql
2750+ local:mysql
2751+
2752+In all cases, a versioned charm URL will be expanded as expected (for example,
2753+mysql-33 becomes cs:precise/mysql-33).
2754+
2755+<service name>, if omitted, will be derived from <charm name>.
2756+`
2757+
2758+func (c *DeployCommand) Info() *cmd.Info {
2759+ return &cmd.Info{
2760+ "deploy", "<charm name> [<service name>]", "deploy a new service", deployDoc,
2761+ }
2762+}
2763+
2764+func (c *DeployCommand) Init(f *gnuflag.FlagSet, args []string) error {
2765+ addEnvironFlags(&c.EnvName, f)
2766+ f.IntVar(&c.NumUnits, "n", 1, "number of service units to deploy")
2767+ f.IntVar(&c.NumUnits, "num-units", 1, "")
2768+ f.BoolVar(&c.Upgrade, "u", false, "increment local charm revision")
2769+ f.BoolVar(&c.Upgrade, "upgrade", false, "")
2770+ f.StringVar(&c.ConfPath, "config", "", "path to yaml-formatted service config")
2771+ f.StringVar(&c.RepoPath, "repository", os.Getenv("JUJU_REPOSITORY"), "local charm repository")
2772+ // TODO --constraints
2773+ if err := f.Parse(true, args); err != nil {
2774+ return err
2775+ }
2776+ args = f.Args()
2777+ switch len(args) {
2778+ case 2:
2779+ c.ServiceName = args[1]
2780+ fallthrough
2781+ case 1:
2782+ c.CharmName = args[0]
2783+ case 0:
2784+ return errors.New("no charm specified")
2785+ default:
2786+ return cmd.CheckEmpty(args[2:])
2787+ }
2788+ if c.NumUnits < 1 {
2789+ return errors.New("must deploy at least one unit")
2790+ }
2791+ return nil
2792+}
2793+
2794+func (c *DeployCommand) Run(ctx *cmd.Context) error {
2795+ panic("not implemented")
2796+}
2797
2798=== added file 'cmd/juju/destroy.go.OTHER'
2799--- cmd/juju/destroy.go.OTHER 1970-01-01 00:00:00 +0000
2800+++ cmd/juju/destroy.go.OTHER 2012-06-07 11:57:18 +0000
2801@@ -0,0 +1,36 @@
2802+package main
2803+
2804+import (
2805+ "launchpad.net/gnuflag"
2806+ "launchpad.net/juju-core/juju/cmd"
2807+ "launchpad.net/juju-core/juju/juju"
2808+)
2809+
2810+// DestroyCommand destroys an environment.
2811+type DestroyCommand struct {
2812+ EnvName string
2813+}
2814+
2815+func (c *DestroyCommand) Info() *cmd.Info {
2816+ return &cmd.Info{
2817+ "destroy-environment", "[options]",
2818+ "terminate all machines and other associated resources for an environment",
2819+ "",
2820+ }
2821+}
2822+
2823+func (c *DestroyCommand) Init(f *gnuflag.FlagSet, args []string) error {
2824+ addEnvironFlags(&c.EnvName, f)
2825+ if err := f.Parse(true, args); err != nil {
2826+ return err
2827+ }
2828+ return cmd.CheckEmpty(f.Args())
2829+}
2830+
2831+func (c *DestroyCommand) Run(_ *cmd.Context) error {
2832+ conn, err := juju.NewConn(c.EnvName)
2833+ if err != nil {
2834+ return err
2835+ }
2836+ return conn.Destroy()
2837+}
2838
2839=== added file 'cmd/juju/main.go.OTHER'
2840--- cmd/juju/main.go.OTHER 1970-01-01 00:00:00 +0000
2841+++ cmd/juju/main.go.OTHER 2012-06-07 11:57:18 +0000
2842@@ -0,0 +1,40 @@
2843+package main
2844+
2845+import (
2846+ "launchpad.net/gnuflag"
2847+ "launchpad.net/juju-core/juju/cmd"
2848+ "os"
2849+)
2850+
2851+// When we import an environment provider implementation
2852+// here, it will register itself with environs, and hence
2853+// be available to the juju command.
2854+import (
2855+ _ "launchpad.net/juju-core/juju/environs/ec2"
2856+)
2857+
2858+var jujuDoc = `
2859+juju provides easy, intelligent service orchestration on top of environments
2860+such as OpenStack, Amazon AWS, or bare metal.
2861+
2862+https://juju.ubuntu.com/
2863+`
2864+
2865+// Main registers subcommands for the juju executable, and hands over control
2866+// to the cmd package. This function is not redundant with main, because it
2867+// provides an entry point for testing with arbitrary command line arguments.
2868+func Main(args []string) {
2869+ juju := &cmd.SuperCommand{Name: "juju", Doc: jujuDoc, Log: &cmd.Log{}}
2870+ juju.Register(&BootstrapCommand{})
2871+ juju.Register(&DestroyCommand{})
2872+ os.Exit(cmd.Main(juju, cmd.DefaultContext(), args[1:]))
2873+}
2874+
2875+func main() {
2876+ Main(os.Args)
2877+}
2878+
2879+func addEnvironFlags(name *string, f *gnuflag.FlagSet) {
2880+ f.StringVar(name, "e", "", "juju environment to operate in")
2881+ f.StringVar(name, "environment", "", "")
2882+}
2883
2884=== added directory 'cmd/jujuc'
2885=== added file 'cmd/jujuc/main.go.OTHER'
2886--- cmd/jujuc/main.go.OTHER 1970-01-01 00:00:00 +0000
2887+++ cmd/jujuc/main.go.OTHER 2012-06-07 11:57:18 +0000
2888@@ -0,0 +1,89 @@
2889+package main
2890+
2891+import (
2892+ "fmt"
2893+ "launchpad.net/juju-core/juju/cmd/jujuc/server"
2894+ "net/rpc"
2895+ "os"
2896+ "path/filepath"
2897+)
2898+
2899+var Help = `
2900+The jujuc command forwards invocations over RPC for execution by the juju
2901+unit agent. It expects to be called via a symlink named for the desired
2902+remote command, and expects JUJU_AGENT_SOCKET and JUJU_CONTEXT_ID be set
2903+in its environment.
2904+`[1:]
2905+
2906+func getenv(name string) (string, error) {
2907+ value := os.Getenv(name)
2908+ if value == "" {
2909+ return "", fmt.Errorf("%s not set", name)
2910+ }
2911+ return value, nil
2912+}
2913+
2914+func getwd() (string, error) {
2915+ dir, err := os.Getwd()
2916+ if err != nil {
2917+ return "", err
2918+ }
2919+ abs, err := filepath.Abs(dir)
2920+ if err != nil {
2921+ return "", err
2922+ }
2923+ return abs, nil
2924+}
2925+
2926+// Main uses JUJU_CONTEXT_ID and JUJU_AGENT_SOCKET to ask a running unit agent
2927+// to execute a Command on our behalf. Individual commands should be exposed
2928+// by symlinking the command name to this executable.
2929+// This function is not redundant with main, because it is exported, and can
2930+// thus be called by testing code.
2931+func Main(args []string) (code int, err error) {
2932+ commandName := filepath.Base(args[0])
2933+ if commandName == "jujuc" {
2934+ fmt.Fprint(os.Stderr, Help)
2935+ return 2, fmt.Errorf("jujuc should not be called directly")
2936+ }
2937+ code = 1
2938+ contextId, err := getenv("JUJU_CONTEXT_ID")
2939+ if err != nil {
2940+ return
2941+ }
2942+ dir, err := getwd()
2943+ if err != nil {
2944+ return
2945+ }
2946+ req := server.Request{
2947+ ContextId: contextId,
2948+ Dir: dir,
2949+ CommandName: commandName,
2950+ Args: args[1:],
2951+ }
2952+ socketPath, err := getenv("JUJU_AGENT_SOCKET")
2953+ if err != nil {
2954+ return
2955+ }
2956+ client, err := rpc.Dial("unix", socketPath)
2957+ if err != nil {
2958+ return
2959+ }
2960+ defer client.Close()
2961+ var resp server.Response
2962+ err = client.Call("Jujuc.Main", req, &resp)
2963+ if err != nil {
2964+ return
2965+ }
2966+ os.Stdout.Write(resp.Stdout)
2967+ os.Stderr.Write(resp.Stderr)
2968+ return resp.Code, nil
2969+}
2970+
2971+func main() {
2972+ code, err := Main(os.Args)
2973+ if err != nil {
2974+ fmt.Fprintf(os.Stderr, "error: %v\n", err)
2975+ }
2976+ os.Exit(code)
2977+}
2978
2979=== added file 'cmd/jujuc/main_test.go.OTHER'
2980--- cmd/jujuc/main_test.go.OTHER 1970-01-01 00:00:00 +0000
2981+++ cmd/jujuc/main_test.go.OTHER 2012-06-07 11:57:18 +0000
2982@@ -0,0 +1,159 @@
2983+package main
2984+
2985+import (
2986+ "errors"
2987+ "flag"
2988+ "fmt"
2989+ "launchpad.net/gnuflag"
2990+ . "launchpad.net/gocheck"
2991+ "launchpad.net/juju-core/juju/cmd"
2992+ "launchpad.net/juju-core/juju/cmd/jujuc/server"
2993+ "os"
2994+ "os/exec"
2995+ "path/filepath"
2996+ "testing"
2997+)
2998+
2999+func Test(t *testing.T) { TestingT(t) }
3000+
3001+var flagRunMain = flag.Bool("run-main", false, "Run the application's main function for recursive testing")
3002+
3003+// Reentrancy point for testing (something as close as possible to) the jujuc
3004+// tool itself.
3005+func TestRunMain(t *testing.T) {
3006+ if *flagRunMain {
3007+ code, err := Main(flag.Args())
3008+ if err != nil {
3009+ fmt.Fprintf(os.Stderr, "error: %v\n", err)
3010+ }
3011+ os.Exit(code)
3012+ }
3013+}
3014+
3015+type RemoteCommand struct {
3016+ msg string
3017+}
3018+
3019+var expectUsage = `usage: remote [options]
3020+purpose: test jujuc
3021+
3022+options:
3023+--error (= "")
3024+ if set, fail
3025+
3026+here is some documentation
3027+`
3028+
3029+func (c *RemoteCommand) Info() *cmd.Info {
3030+ return &cmd.Info{
3031+ "remote", "", "test jujuc", "here is some documentation"}
3032+}
3033+
3034+func (c *RemoteCommand) Init(f *gnuflag.FlagSet, args []string) error {
3035+ f.StringVar(&c.msg, "error", "", "if set, fail")
3036+ if err := f.Parse(true, args); err != nil {
3037+ return err
3038+ }
3039+ return cmd.CheckEmpty(f.Args())
3040+}
3041+
3042+func (c *RemoteCommand) Run(ctx *cmd.Context) error {
3043+ if c.msg != "" {
3044+ return errors.New(c.msg)
3045+ }
3046+ fmt.Fprintf(ctx.Stdout, "success!\n")
3047+ return nil
3048+}
3049+
3050+func run(c *C, sockPath string, contextId string, exit int, cmd ...string) string {
3051+ args := append([]string{"-test.run", "TestRunMain", "-run-main", "--"}, cmd...)
3052+ ps := exec.Command(os.Args[0], args...)
3053+ ps.Dir = c.MkDir()
3054+ ps.Env = []string{
3055+ fmt.Sprintf("JUJU_AGENT_SOCKET=%s", sockPath),
3056+ fmt.Sprintf("JUJU_CONTEXT_ID=%s", contextId),
3057+ }
3058+ output, err := ps.CombinedOutput()
3059+ if exit == 0 {
3060+ c.Assert(err, IsNil)
3061+ } else {
3062+ c.Assert(err, ErrorMatches, fmt.Sprintf("exit status %d", exit))
3063+ }
3064+ return string(output)
3065+}
3066+
3067+type MainSuite struct {
3068+ sockPath string
3069+ server *server.Server
3070+}
3071+
3072+var _ = Suite(&MainSuite{})
3073+
3074+func (s *MainSuite) SetUpSuite(c *C) {
3075+ factory := func(contextId, cmdName string) (cmd.Command, error) {
3076+ if contextId != "bill" {
3077+ return nil, fmt.Errorf("bad context: %s", contextId)
3078+ }
3079+ if cmdName != "remote" {
3080+ return nil, fmt.Errorf("bad command: %s", cmdName)
3081+ }
3082+ return &RemoteCommand{}, nil
3083+ }
3084+ s.sockPath = filepath.Join(c.MkDir(), "test.sock")
3085+ srv, err := server.NewServer(factory, s.sockPath)
3086+ c.Assert(err, IsNil)
3087+ s.server = srv
3088+ go func() {
3089+ if err := s.server.Run(); err != nil {
3090+ c.Fatalf("server died: %s", err)
3091+ }
3092+ }()
3093+}
3094+
3095+func (s *MainSuite) TearDownSuite(c *C) {
3096+ s.server.Close()
3097+}
3098+
3099+var argsTests = []struct {
3100+ args []string
3101+ code int
3102+ output string
3103+}{
3104+ {[]string{"jujuc", "whatever"}, 2, Help + "error: jujuc should not be called directly\n"},
3105+ {[]string{"remote"}, 0, "success!\n"},
3106+ {[]string{"/path/to/remote"}, 0, "success!\n"},
3107+ {[]string{"unknown"}, 1, "error: bad request: bad command: unknown\n"},
3108+ {[]string{"remote", "--error", "borken"}, 1, "error: borken\n"},
3109+ {[]string{"remote", "--unknown"}, 2, expectUsage + "error: flag provided but not defined: --unknown\n"},
3110+ {[]string{"remote", "unwanted"}, 2, expectUsage + `error: unrecognized args: ["unwanted"]` + "\n"},
3111+}
3112+
3113+func (s *MainSuite) TestArgs(c *C) {
3114+ for _, t := range argsTests {
3115+ fmt.Println(t.args)
3116+ output := run(c, s.sockPath, "bill", t.code, t.args...)
3117+ c.Assert(output, Equals, t.output)
3118+ }
3119+}
3120+
3121+func (s *MainSuite) TestNoClientId(c *C) {
3122+ output := run(c, s.sockPath, "", 1, "remote")
3123+ c.Assert(output, Equals, "error: JUJU_CONTEXT_ID not set\n")
3124+}
3125+
3126+func (s *MainSuite) TestBadClientId(c *C) {
3127+ output := run(c, s.sockPath, "ben", 1, "remote")
3128+ c.Assert(output, Equals, "error: bad request: bad context: ben\n")
3129+}
3130+
3131+func (s *MainSuite) TestNoSockPath(c *C) {
3132+ output := run(c, "", "bill", 1, "remote")
3133+ c.Assert(output, Equals, "error: JUJU_AGENT_SOCKET not set\n")
3134+}
3135+
3136+func (s *MainSuite) TestBadSockPath(c *C) {
3137+ badSock := filepath.Join(c.MkDir(), "bad.sock")
3138+ output := run(c, badSock, "bill", 1, "remote")
3139+ err := fmt.Sprintf("error: dial unix %s: .*\n", badSock)
3140+ c.Assert(output, Matches, err)
3141+}
3142
3143=== added directory 'cmd/jujuc/server'
3144=== added file 'cmd/jujuc/server/config-get.go.OTHER'
3145--- cmd/jujuc/server/config-get.go.OTHER 1970-01-01 00:00:00 +0000
3146+++ cmd/jujuc/server/config-get.go.OTHER 2012-06-07 11:57:18 +0000
3147@@ -0,0 +1,66 @@
3148+package server
3149+
3150+import (
3151+ "launchpad.net/gnuflag"
3152+ "launchpad.net/juju-core/juju/cmd"
3153+)
3154+
3155+// ConfigGetCommand implements the config-get command.
3156+type ConfigGetCommand struct {
3157+ *ClientContext
3158+ Key string // The key to show. If empty, show all.
3159+ out output
3160+}
3161+
3162+func NewConfigGetCommand(ctx *ClientContext) (cmd.Command, error) {
3163+ if err := ctx.check(); err != nil {
3164+ return nil, err
3165+ }
3166+ return &ConfigGetCommand{ClientContext: ctx}, nil
3167+}
3168+
3169+func (c *ConfigGetCommand) Info() *cmd.Info {
3170+ return &cmd.Info{
3171+ "config-get", "[<key>]",
3172+ "print service configuration",
3173+ "If a key is given, only the value for that key will be printed.",
3174+ }
3175+}
3176+
3177+func (c *ConfigGetCommand) Init(f *gnuflag.FlagSet, args []string) error {
3178+ c.out.addFlags(f, "yaml", defaultFormatters)
3179+ if err := f.Parse(true, args); err != nil {
3180+ return err
3181+ }
3182+ args = f.Args()
3183+ if args == nil {
3184+ return nil
3185+ }
3186+ c.Key = args[0]
3187+ return cmd.CheckEmpty(args[1:])
3188+}
3189+
3190+func (c *ConfigGetCommand) Run(ctx *cmd.Context) error {
3191+ unit, err := c.State.Unit(c.LocalUnitName)
3192+ if err != nil {
3193+ return err
3194+ }
3195+ service, err := c.State.Service(unit.ServiceName())
3196+ if err != nil {
3197+ return err
3198+ }
3199+ conf, err := service.Config()
3200+ if err != nil {
3201+ return err
3202+ }
3203+ var value interface{}
3204+ if c.Key == "" {
3205+ value = conf.Map()
3206+ } else {
3207+ value, _ = conf.Get(c.Key)
3208+ }
3209+ if c.out.testMode {
3210+ return truthError(value)
3211+ }
3212+ return c.out.write(ctx, value)
3213+}
3214
3215=== added file 'cmd/jujuc/server/config-get_test.go.OTHER'
3216--- cmd/jujuc/server/config-get_test.go.OTHER 1970-01-01 00:00:00 +0000
3217+++ cmd/jujuc/server/config-get_test.go.OTHER 2012-06-07 11:57:18 +0000
3218@@ -0,0 +1,125 @@
3219+package server_test
3220+
3221+import (
3222+ "io/ioutil"
3223+ . "launchpad.net/gocheck"
3224+ "launchpad.net/juju-core/juju/cmd"
3225+ "path/filepath"
3226+)
3227+
3228+type ConfigGetSuite struct {
3229+ UnitFixture
3230+}
3231+
3232+var _ = Suite(&ConfigGetSuite{})
3233+
3234+func (s *ConfigGetSuite) SetUpTest(c *C) {
3235+ s.UnitFixture.SetUpTest(c)
3236+ conf, err := s.service.Config()
3237+ c.Assert(err, IsNil)
3238+ conf.Update(map[string]interface{}{
3239+ "monsters": false,
3240+ "spline-reticulation": 45.0,
3241+ })
3242+ _, err = conf.Write()
3243+ c.Assert(err, IsNil)
3244+}
3245+
3246+var configGetYamlMap = "(spline-reticulation: 45\nmonsters: false\n|monsters: false\nspline-reticulation: 45\n)\n"
3247+var configGetTests = []struct {
3248+ args []string
3249+ out string
3250+}{
3251+ {[]string{"monsters"}, "false\n\n"},
3252+ {[]string{"--format", "yaml", "monsters"}, "false\n\n"},
3253+ {[]string{"--format", "json", "monsters"}, "false\n"},
3254+ {[]string{"spline-reticulation"}, "45\n\n"},
3255+ {[]string{"--format", "yaml", "spline-reticulation"}, "45\n\n"},
3256+ {[]string{"--format", "json", "spline-reticulation"}, "45\n"},
3257+ {[]string{"missing"}, ""},
3258+ {[]string{"--format", "yaml", "missing"}, ""},
3259+ {[]string{"--format", "json", "missing"}, "null\n"},
3260+ {nil, configGetYamlMap},
3261+ {[]string{"--format", "yaml"}, configGetYamlMap},
3262+ {[]string{"--format", "json"}, `{"monsters":false,"spline-reticulation":45}` + "\n"},
3263+}
3264+
3265+func (s *ConfigGetSuite) TestOutputFormat(c *C) {
3266+ for _, t := range configGetTests {
3267+ com, err := s.ctx.NewCommand("config-get")
3268+ c.Assert(err, IsNil)
3269+ ctx := dummyContext(c)
3270+ code := cmd.Main(com, ctx, t.args)
3271+ c.Assert(code, Equals, 0)
3272+ c.Assert(bufferString(ctx.Stderr), Equals, "")
3273+ c.Assert(bufferString(ctx.Stdout), Matches, t.out)
3274+ }
3275+}
3276+
3277+var configGetTestModeTests = []struct {
3278+ args []string
3279+ code int
3280+}{
3281+ {[]string{"monsters", "--test"}, 1},
3282+ {[]string{"spline-reticulation", "--test"}, 0},
3283+ {[]string{"missing", "--test"}, 1},
3284+ {[]string{"--test"}, 0},
3285+}
3286+
3287+func (s *ConfigGetSuite) TestTestMode(c *C) {
3288+ for _, t := range configGetTestModeTests {
3289+ com, err := s.ctx.NewCommand("config-get")
3290+ c.Assert(err, IsNil)
3291+ ctx := dummyContext(c)
3292+ code := cmd.Main(com, ctx, t.args)
3293+ c.Assert(code, Equals, t.code)
3294+ c.Assert(bufferString(ctx.Stderr), Equals, "")
3295+ c.Assert(bufferString(ctx.Stdout), Equals, "")
3296+ }
3297+}
3298+
3299+func (s *ConfigGetSuite) TestHelp(c *C) {
3300+ com, err := s.ctx.NewCommand("config-get")
3301+ c.Assert(err, IsNil)
3302+ ctx := dummyContext(c)
3303+ code := cmd.Main(com, ctx, []string{"--help"})
3304+ c.Assert(code, Equals, 0)
3305+ c.Assert(bufferString(ctx.Stdout), Equals, "")
3306+ c.Assert(bufferString(ctx.Stderr), Equals, `usage: config-get [options] [<key>]
3307+purpose: print service configuration
3308+
3309+options:
3310+--format (= yaml)
3311+ specify output format (json|yaml)
3312+-o, --output (= "")
3313+ specify an output file
3314+--test (= false)
3315+ returns non-zero exit code if value is false/zero/empty
3316+
3317+If a key is given, only the value for that key will be printed.
3318+`)
3319+}
3320+
3321+func (s *ConfigGetSuite) TestOutputPath(c *C) {
3322+ com, err := s.ctx.NewCommand("config-get")
3323+ c.Assert(err, IsNil)
3324+ ctx := dummyContext(c)
3325+ code := cmd.Main(com, ctx, []string{"--output", "some-file", "monsters"})
3326+ c.Assert(code, Equals, 0)
3327+ c.Assert(bufferString(ctx.Stderr), Equals, "")
3328+ c.Assert(bufferString(ctx.Stdout), Equals, "")
3329+ content, err := ioutil.ReadFile(filepath.Join(ctx.Dir, "some-file"))
3330+ c.Assert(err, IsNil)
3331+ c.Assert(string(content), Equals, "false\n\n")
3332+}
3333+
3334+func (s *ConfigGetSuite) TestUnknownArg(c *C) {
3335+ com, err := s.ctx.NewCommand("config-get")
3336+ c.Assert(err, IsNil)
3337+ err = com.Init(dummyFlagSet(), []string{"multiple", "keys"})
3338+ c.Assert(err, ErrorMatches, `unrecognized args: \["keys"\]`)
3339+}
3340+
3341+func (s *ConfigGetSuite) TestUnitCommand(c *C) {
3342+ s.AssertUnitCommand(c, "config-get")
3343+}
3344
3345=== added file 'cmd/jujuc/server/context.go.OTHER'
3346--- cmd/jujuc/server/context.go.OTHER 1970-01-01 00:00:00 +0000
3347+++ cmd/jujuc/server/context.go.OTHER 2012-06-07 11:57:18 +0000
3348@@ -0,0 +1,96 @@
3349+// The cmd/jujuc/server package implements the server side of the jujuc proxy
3350+// tool, which forwards command invocations to the unit agent process so that
3351+// they can be executed against specific state.
3352+package server
3353+
3354+import (
3355+ "fmt"
3356+ "launchpad.net/juju-core/juju/cmd"
3357+ "launchpad.net/juju-core/juju/state"
3358+ "os"
3359+ "os/exec"
3360+ "path/filepath"
3361+)
3362+
3363+// ClientContext is responsible for the state against which a jujuc-forwarded
3364+// command will execute; it implements the core of the various jujuc tools, and
3365+// is involved in constructing a suitable environment in which to execute a hook
3366+// (which is likely to call jujuc tools that need this specific ClientContext).
3367+type ClientContext struct {
3368+ Id string
3369+ State *state.State
3370+ LocalUnitName string
3371+ RemoteUnitName string
3372+ RelationName string
3373+}
3374+
3375+// checkUnitState returns an error if ctx has nil State or LocalUnitName fields.
3376+func (ctx *ClientContext) check() error {
3377+ if ctx.State == nil {
3378+ return fmt.Errorf("context %s cannot access state", ctx.Id)
3379+ }
3380+ if ctx.LocalUnitName == "" {
3381+ return fmt.Errorf("context %s is not attached to a unit", ctx.Id)
3382+ }
3383+ return nil
3384+}
3385+
3386+// newCommands maps Command names to initializers.
3387+var newCommands = map[string]func(*ClientContext) (cmd.Command, error){
3388+ "close-port": NewClosePortCommand,
3389+ "config-get": NewConfigGetCommand,
3390+ "juju-log": NewJujuLogCommand,
3391+ "open-port": NewOpenPortCommand,
3392+ "unit-get": NewUnitGetCommand,
3393+}
3394+
3395+// NewCommand returns an instance of the named Command, initialized to execute
3396+// against this ClientContext.
3397+func (ctx *ClientContext) NewCommand(name string) (cmd.Command, error) {
3398+ f := newCommands[name]
3399+ if f == nil {
3400+ return nil, fmt.Errorf("unknown command: %s", name)
3401+ }
3402+ return f(ctx)
3403+}
3404+
3405+// hookVars returns an os.Environ-style list of strings necessary to run a hook
3406+// such that it can know what environment it's operating in, and can call back
3407+// into ctx.
3408+func (ctx *ClientContext) hookVars(charmDir, socketPath string) []string {
3409+ vars := []string{
3410+ "APT_LISTCHANGES_FRONTEND=none",
3411+ "DEBIAN_FRONTEND=noninteractive",
3412+ "PATH=" + os.Getenv("PATH"),
3413+ "CHARM_DIR=" + charmDir,
3414+ "JUJU_CONTEXT_ID=" + ctx.Id,
3415+ "JUJU_AGENT_SOCKET=" + socketPath,
3416+ }
3417+ if ctx.LocalUnitName != "" {
3418+ vars = append(vars, "JUJU_UNIT_NAME="+ctx.LocalUnitName)
3419+ }
3420+ if ctx.RemoteUnitName != "" {
3421+ vars = append(vars, "JUJU_REMOTE_UNIT="+ctx.RemoteUnitName)
3422+ }
3423+ if ctx.RelationName != "" {
3424+ vars = append(vars, "JUJU_RELATION="+ctx.RelationName)
3425+ }
3426+ return vars
3427+}
3428+
3429+// RunHook executes a hook in an environment which allows it to to call back
3430+// into ctx to execute jujuc tools.
3431+func (ctx *ClientContext) RunHook(hookName, charmDir, socketPath string) error {
3432+ ps := exec.Command(filepath.Join(charmDir, "hooks", hookName))
3433+ ps.Env = ctx.hookVars(charmDir, socketPath)
3434+ ps.Dir = charmDir
3435+ if err := ps.Run(); err != nil {
3436+ if ee, ok := err.(*exec.Error); ok {
3437+ if os.IsNotExist(ee.Err) {
3438+ return nil
3439+ }
3440+ }
3441+ return err
3442+ }
3443+ return nil
3444+}
3445
3446=== added file 'cmd/jujuc/server/context_test.go.OTHER'
3447--- cmd/jujuc/server/context_test.go.OTHER 1970-01-01 00:00:00 +0000
3448+++ cmd/jujuc/server/context_test.go.OTHER 2012-06-07 11:57:18 +0000
3449@@ -0,0 +1,145 @@
3450+package server_test
3451+
3452+import (
3453+ "fmt"
3454+ "io/ioutil"
3455+ . "launchpad.net/gocheck"
3456+ "launchpad.net/juju-core/juju/cmd/jujuc/server"
3457+ "launchpad.net/juju-core/juju/state"
3458+ "os"
3459+ "path/filepath"
3460+ "strings"
3461+)
3462+
3463+type GetCommandSuite struct{}
3464+
3465+var _ = Suite(&GetCommandSuite{})
3466+
3467+var getCommandTests = []struct {
3468+ name string
3469+ err string
3470+}{
3471+ {"close-port", ""},
3472+ {"config-get", ""},
3473+ {"juju-log", ""},
3474+ {"open-port", ""},
3475+ {"unit-get", ""},
3476+ {"random", "unknown command: random"},
3477+}
3478+
3479+func (s *GetCommandSuite) TestGetCommand(c *C) {
3480+ ctx := &server.ClientContext{
3481+ Id: "ctxid",
3482+ State: &state.State{},
3483+ LocalUnitName: "minecraft/0",
3484+ }
3485+ for _, t := range getCommandTests {
3486+ com, err := ctx.NewCommand(t.name)
3487+ if t.err == "" {
3488+ // At this level, just check basic sanity; commands are tested in
3489+ // more detail elsewhere.
3490+ c.Assert(err, IsNil)
3491+ c.Assert(com.Info().Name, Equals, t.name)
3492+ } else {
3493+ c.Assert(com, IsNil)
3494+ c.Assert(err, ErrorMatches, t.err)
3495+ }
3496+ }
3497+}
3498+
3499+type RunHookSuite struct {
3500+ outPath string
3501+}
3502+
3503+var _ = Suite(&RunHookSuite{})
3504+
3505+// makeCharm constructs a fake charm dir containing a single named hook with
3506+// permissions perm and exit code code. It returns the charm directory and the
3507+// path to which the hook script will write environment variables.
3508+func makeCharm(c *C, hookName string, perm os.FileMode, code int) (charmDir, outPath string) {
3509+ charmDir = c.MkDir()
3510+ hooksDir := filepath.Join(charmDir, "hooks")
3511+ err := os.Mkdir(hooksDir, 0755)
3512+ c.Assert(err, IsNil)
3513+ hook, err := os.OpenFile(filepath.Join(hooksDir, hookName), os.O_CREATE|os.O_WRONLY, perm)
3514+ c.Assert(err, IsNil)
3515+ defer hook.Close()
3516+ outPath = filepath.Join(c.MkDir(), "hook.out")
3517+ _, err = fmt.Fprintf(hook, "#!/bin/bash\nenv > %s\nexit %d", outPath, code)
3518+ c.Assert(err, IsNil)
3519+ return charmDir, outPath
3520+}
3521+
3522+func AssertEnvContains(c *C, lines []string, env map[string]string) {
3523+ for k, v := range env {
3524+ sought := k + "=" + v
3525+ found := false
3526+ for _, line := range lines {
3527+ if line == sought {
3528+ found = true
3529+ continue
3530+ }
3531+ }
3532+ comment := Commentf("expected to find %v among %v", sought, lines)
3533+ c.Assert(found, Equals, true, comment)
3534+ }
3535+}
3536+
3537+func AssertEnv(c *C, outPath string, env map[string]string) {
3538+ out, err := ioutil.ReadFile(outPath)
3539+ c.Assert(err, IsNil)
3540+ lines := strings.Split(string(out), "\n")
3541+ AssertEnvContains(c, lines, env)
3542+ AssertEnvContains(c, lines, map[string]string{
3543+ "PATH": os.Getenv("PATH"),
3544+ "DEBIAN_FRONTEND": "noninteractive",
3545+ "APT_LISTCHANGES_FRONTEND": "none",
3546+ })
3547+}
3548+
3549+func (s *RunHookSuite) TestNoHook(c *C) {
3550+ ctx := &server.ClientContext{}
3551+ err := ctx.RunHook("tree-fell-in-forest", c.MkDir(), "")
3552+ c.Assert(err, IsNil)
3553+}
3554+
3555+func (s *RunHookSuite) TestNonExecutableHook(c *C) {
3556+ ctx := &server.ClientContext{}
3557+ charmDir, _ := makeCharm(c, "something-happened", 0600, 0)
3558+ err := ctx.RunHook("something-happened", charmDir, "")
3559+ c.Assert(err, ErrorMatches, `exec: ".*/something-happened": permission denied`)
3560+}
3561+
3562+func (s *RunHookSuite) TestBadHook(c *C) {
3563+ ctx := &server.ClientContext{Id: "ctx-id"}
3564+ charmDir, outPath := makeCharm(c, "occurrence-occurred", 0700, 99)
3565+ socketPath := "/path/to/socket"
3566+ err := ctx.RunHook("occurrence-occurred", charmDir, socketPath)
3567+ c.Assert(err, ErrorMatches, "exit status 99")
3568+ AssertEnv(c, outPath, map[string]string{
3569+ "CHARM_DIR": charmDir,
3570+ "JUJU_AGENT_SOCKET": socketPath,
3571+ "JUJU_CONTEXT_ID": "ctx-id",
3572+ })
3573+}
3574+
3575+func (s *RunHookSuite) TestGoodHookWithVars(c *C) {
3576+ ctx := &server.ClientContext{
3577+ Id: "some-id",
3578+ LocalUnitName: "local/99",
3579+ RemoteUnitName: "remote/123",
3580+ RelationName: "rel",
3581+ }
3582+ charmDir, outPath := makeCharm(c, "something-happened", 0700, 0)
3583+ socketPath := "/path/to/socket"
3584+ err := ctx.RunHook("something-happened", charmDir, socketPath)
3585+ c.Assert(err, IsNil)
3586+ AssertEnv(c, outPath, map[string]string{
3587+ "CHARM_DIR": charmDir,
3588+ "JUJU_AGENT_SOCKET": socketPath,
3589+ "JUJU_CONTEXT_ID": "some-id",
3590+ "JUJU_UNIT_NAME": "local/99",
3591+ "JUJU_REMOTE_UNIT": "remote/123",
3592+ "JUJU_RELATION": "rel",
3593+ })
3594+}
3595
3596=== added file 'cmd/jujuc/server/juju-log.go.OTHER'
3597--- cmd/jujuc/server/juju-log.go.OTHER 1970-01-01 00:00:00 +0000
3598+++ cmd/jujuc/server/juju-log.go.OTHER 2012-06-07 11:57:18 +0000
3599@@ -0,0 +1,57 @@
3600+package server
3601+
3602+import (
3603+ "errors"
3604+ "launchpad.net/gnuflag"
3605+ "launchpad.net/juju-core/juju/cmd"
3606+ "launchpad.net/juju-core/juju/log"
3607+ "strings"
3608+)
3609+
3610+// JujuLogCommand implements the juju-log command.
3611+type JujuLogCommand struct {
3612+ *ClientContext
3613+ Message string
3614+ Debug bool
3615+}
3616+
3617+func NewJujuLogCommand(ctx *ClientContext) (cmd.Command, error) {
3618+ return &JujuLogCommand{ClientContext: ctx}, nil
3619+}
3620+
3621+func (c *JujuLogCommand) Info() *cmd.Info {
3622+ return &cmd.Info{"juju-log", "<message>", "write a message to the juju log", ""}
3623+}
3624+
3625+func (c *JujuLogCommand) Init(f *gnuflag.FlagSet, args []string) error {
3626+ f.BoolVar(&c.Debug, "debug", false, "log at debug level")
3627+ if err := f.Parse(true, args); err != nil {
3628+ return err
3629+ }
3630+ args = f.Args()
3631+ if args == nil {
3632+ return errors.New("no message specified")
3633+ }
3634+ c.Message = strings.Join(args, " ")
3635+ return nil
3636+}
3637+
3638+func (c *JujuLogCommand) Run(_ *cmd.Context) error {
3639+ s := []string{}
3640+ if c.LocalUnitName != "" {
3641+ s = append(s, c.LocalUnitName)
3642+ }
3643+ if c.RelationName != "" {
3644+ s = append(s, c.RelationName)
3645+ }
3646+ msg := c.Message
3647+ if len(s) > 0 {
3648+ msg = strings.Join(s, " ") + ": " + msg
3649+ }
3650+ if c.Debug {
3651+ log.Debugf("%s", msg)
3652+ } else {
3653+ log.Printf("%s", msg)
3654+ }
3655+ return nil
3656+}
3657
3658=== added file 'cmd/jujuc/server/juju-log_test.go.OTHER'
3659--- cmd/jujuc/server/juju-log_test.go.OTHER 1970-01-01 00:00:00 +0000
3660+++ cmd/jujuc/server/juju-log_test.go.OTHER 2012-06-07 11:57:18 +0000
3661@@ -0,0 +1,82 @@
3662+package server_test
3663+
3664+import (
3665+ "bytes"
3666+ "fmt"
3667+ "launchpad.net/gnuflag"
3668+ . "launchpad.net/gocheck"
3669+ "launchpad.net/juju-core/juju/cmd"
3670+ "launchpad.net/juju-core/juju/cmd/jujuc/server"
3671+ "launchpad.net/juju-core/juju/log"
3672+ stdlog "log"
3673+)
3674+
3675+type JujuLogSuite struct{}
3676+
3677+var _ = Suite(&JujuLogSuite{})
3678+
3679+func pushLog(debug bool) (buf *bytes.Buffer, pop func()) {
3680+ oldTarget, oldDebug := log.Target, log.Debug
3681+ buf = new(bytes.Buffer)
3682+ log.Target, log.Debug = stdlog.New(buf, "", 0), debug
3683+ return buf, func() {
3684+ log.Target, log.Debug = oldTarget, oldDebug
3685+ }
3686+}
3687+
3688+func dummyFlagSet() *gnuflag.FlagSet {
3689+ return gnuflag.NewFlagSet("", gnuflag.ContinueOnError)
3690+}
3691+
3692+var commonLogTests = []struct {
3693+ debugEnabled bool
3694+ debugFlag bool
3695+ target string
3696+}{
3697+ {false, false, "JUJU"},
3698+ {false, true, ""},
3699+ {true, false, "JUJU"},
3700+ {true, true, "JUJU:DEBUG"},
3701+}
3702+
3703+func assertLogs(c *C, ctx *server.ClientContext, badge string) {
3704+ msg1 := "the chickens"
3705+ msg2 := "are 110% AWESOME"
3706+ com, err := ctx.NewCommand("juju-log")
3707+ c.Assert(err, IsNil)
3708+ for _, t := range commonLogTests {
3709+ buf, pop := pushLog(t.debugEnabled)
3710+ defer pop()
3711+
3712+ var args []string
3713+ if t.debugFlag {
3714+ args = []string{"--debug", msg1, msg2}
3715+ } else {
3716+ args = []string{msg1, msg2}
3717+ }
3718+ code := cmd.Main(com, &cmd.Context{}, args)
3719+ c.Assert(code, Equals, 0)
3720+
3721+ if t.target == "" {
3722+ c.Assert(buf.String(), Equals, "")
3723+ } else {
3724+ expect := fmt.Sprintf("%s %s: %s %s\n", t.target, badge, msg1, msg2)
3725+ c.Assert(buf.String(), Equals, expect)
3726+ }
3727+ }
3728+}
3729+
3730+func (s *JujuLogSuite) TestBadges(c *C) {
3731+ local := &server.ClientContext{LocalUnitName: "minecraft/0"}
3732+ assertLogs(c, local, "minecraft/0")
3733+ relation := &server.ClientContext{LocalUnitName: "minecraft/0", RelationName: "bot"}
3734+ assertLogs(c, relation, "minecraft/0 bot")
3735+}
3736+
3737+func (s *JujuLogSuite) TestRequiresMessage(c *C) {
3738+ ctx := &server.ClientContext{}
3739+ com, err := ctx.NewCommand("juju-log")
3740+ c.Assert(err, IsNil)
3741+ err = com.Init(dummyFlagSet(), nil)
3742+ c.Assert(err, ErrorMatches, "no message specified")
3743+}
3744
3745=== added file 'cmd/jujuc/server/output.go.OTHER'
3746--- cmd/jujuc/server/output.go.OTHER 1970-01-01 00:00:00 +0000
3747+++ cmd/jujuc/server/output.go.OTHER 2012-06-07 11:57:18 +0000
3748@@ -0,0 +1,149 @@
3749+package server
3750+
3751+import (
3752+ "encoding/json"
3753+ "fmt"
3754+ "io"
3755+ "launchpad.net/gnuflag"
3756+ "launchpad.net/goyaml"
3757+ "launchpad.net/juju-core/juju/cmd"
3758+ "os"
3759+ "reflect"
3760+ "sort"
3761+ "strings"
3762+)
3763+
3764+// formatter converts an arbitrary object into a []byte.
3765+type formatter func(value interface{}) ([]byte, error)
3766+
3767+// formatYaml marshals value to a yaml-formatted []byte, unless value is nil.
3768+func formatYaml(value interface{}) ([]byte, error) {
3769+ if value == nil {
3770+ return nil, nil
3771+ }
3772+ return goyaml.Marshal(value)
3773+}
3774+
3775+// defaultFormatters are used by many jujuc Commands.
3776+var defaultFormatters = map[string]formatter{
3777+ "yaml": formatYaml,
3778+ "json": json.Marshal,
3779+}
3780+
3781+// formatterValue implements gnuflag.Value for the --format flag.
3782+type formatterValue struct {
3783+ name string
3784+ formatters map[string]formatter
3785+}
3786+
3787+// newFormatterValue returns a new formatterValue. The initial formatter name
3788+// must be present in formatters.
3789+func newFormatterValue(initial string, formatters map[string]formatter) *formatterValue {
3790+ v := &formatterValue{formatters: formatters}
3791+ if err := v.Set(initial); err != nil {
3792+ panic(err)
3793+ }
3794+ return v
3795+}
3796+
3797+// Set stores the chosen formatter name in v.name.
3798+func (v *formatterValue) Set(value string) error {
3799+ if v.formatters[value] == nil {
3800+ return fmt.Errorf("unknown format: %s", value)
3801+ }
3802+ v.name = value
3803+ return nil
3804+}
3805+
3806+// String returns the chosen formatter name.
3807+func (v *formatterValue) String() string {
3808+ return v.name
3809+}
3810+
3811+// doc returns documentation for the --format flag.
3812+func (v *formatterValue) doc() string {
3813+ choices := make([]string, len(v.formatters))
3814+ i := 0
3815+ for name := range v.formatters {
3816+ choices[i] = name
3817+ i++
3818+ }
3819+ sort.Strings(choices)
3820+ return "specify output format (" + strings.Join(choices, "|") + ")"
3821+}
3822+
3823+// format runs the chosen formatter on value.
3824+func (v *formatterValue) format(value interface{}) ([]byte, error) {
3825+ return v.formatters[v.name](value)
3826+}
3827+
3828+// output is responsible for interpreting output-related command line flags
3829+// and writing a value to a file or to stdout as directed. The testMode field,
3830+// controlled by the --test flag, is used to indicate that output should be
3831+// suppressed and communicated entirely in the process exit code.
3832+type output struct {
3833+ formatter *formatterValue
3834+ outPath string
3835+ testMode bool
3836+}
3837+
3838+// addFlags injects appropriate command line flags into f.
3839+func (c *output) addFlags(f *gnuflag.FlagSet, name string, formatters map[string]formatter) {
3840+ c.formatter = newFormatterValue(name, formatters)
3841+ f.Var(c.formatter, "format", c.formatter.doc())
3842+ f.StringVar(&c.outPath, "o", "", "specify an output file")
3843+ f.StringVar(&c.outPath, "output", "", "")
3844+ f.BoolVar(&c.testMode, "test", false, "returns non-zero exit code if value is false/zero/empty")
3845+}
3846+
3847+// write formats and outputs value as directed by the --format and --output
3848+// command line flags.
3849+func (c *output) write(ctx *cmd.Context, value interface{}) (err error) {
3850+ var target io.Writer
3851+ if c.outPath == "" {
3852+ target = ctx.Stdout
3853+ } else {
3854+ path := ctx.AbsPath(c.outPath)
3855+ if target, err = os.Create(path); err != nil {
3856+ return
3857+ }
3858+ }
3859+ bytes, err := c.formatter.format(value)
3860+ if err != nil {
3861+ return
3862+ }
3863+ if bytes != nil {
3864+ _, err = target.Write(bytes)
3865+ if err == nil {
3866+ _, err = target.Write([]byte{'\n'})
3867+ }
3868+ }
3869+ return
3870+}
3871+
3872+// truthError returns cmd.ErrSilent if value is nil, false, or 0, or an empty
3873+// array, map, slice, or string.
3874+func truthError(value interface{}) error {
3875+ b := true
3876+ v := reflect.ValueOf(value)
3877+ switch v.Kind() {
3878+ case reflect.Invalid:
3879+ b = false
3880+ case reflect.Bool:
3881+ b = v.Bool()
3882+ case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
3883+ b = v.Len() != 0
3884+ case reflect.Float32, reflect.Float64:
3885+ b = v.Float() != 0
3886+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
3887+ b = v.Int() != 0
3888+ case reflect.Uint, reflect.Uintptr, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
3889+ b = v.Uint() != 0
3890+ case reflect.Interface, reflect.Ptr:
3891+ b = !v.IsNil()
3892+ }
3893+ if b {
3894+ return nil
3895+ }
3896+ return cmd.ErrSilent
3897+}
3898
3899=== added file 'cmd/jujuc/server/ports.go.OTHER'
3900--- cmd/jujuc/server/ports.go.OTHER 1970-01-01 00:00:00 +0000
3901+++ cmd/jujuc/server/ports.go.OTHER 2012-06-07 11:57:18 +0000
3902@@ -0,0 +1,100 @@
3903+package server
3904+
3905+import (
3906+ "errors"
3907+ "fmt"
3908+ "launchpad.net/gnuflag"
3909+ "launchpad.net/juju-core/juju/cmd"
3910+ "launchpad.net/juju-core/juju/state"
3911+ "strconv"
3912+ "strings"
3913+)
3914+
3915+const portFormat = "<port>[/<protocol>]"
3916+
3917+// portCommand implements the open-port and close-port commands.
3918+type portCommand struct {
3919+ *ClientContext
3920+ info *cmd.Info
3921+ action func(*state.Unit, string, int) error
3922+ Protocol string
3923+ Port int
3924+}
3925+
3926+func (c *portCommand) Info() *cmd.Info {
3927+ return c.info
3928+}
3929+
3930+func badPort(value interface{}) error {
3931+ return fmt.Errorf(`port must be in the range [1, 65535]; got "%v"`, value)
3932+}
3933+
3934+func (c *portCommand) Init(f *gnuflag.FlagSet, args []string) error {
3935+ if err := f.Parse(true, args); err != nil {
3936+ return err
3937+ }
3938+ args = f.Args()
3939+ if args == nil {
3940+ return errors.New("no port specified")
3941+ }
3942+ parts := strings.Split(args[0], "/")
3943+ if len(parts) > 2 {
3944+ return fmt.Errorf("expected %s; got %q", portFormat, args[0])
3945+ }
3946+ port, err := strconv.Atoi(parts[0])
3947+ if err != nil {
3948+ return badPort(parts[0])
3949+ }
3950+ if port < 1 || port > 65535 {
3951+ return badPort(port)
3952+ }
3953+ protocol := "tcp"
3954+ if len(parts) == 2 {
3955+ protocol = strings.ToLower(parts[1])
3956+ if protocol != "tcp" && protocol != "udp" {
3957+ return fmt.Errorf(`protocol must be "tcp" or "udp"; got %q`, protocol)
3958+ }
3959+ }
3960+ c.Port = port
3961+ c.Protocol = protocol
3962+ return cmd.CheckEmpty(args[1:])
3963+}
3964+
3965+func (c *portCommand) Run(_ *cmd.Context) error {
3966+ unit, err := c.State.Unit(c.LocalUnitName)
3967+ if err != nil {
3968+ return err
3969+ }
3970+ return c.action(unit, c.Protocol, c.Port)
3971+}
3972+
3973+var openPortInfo = &cmd.Info{
3974+ "open-port", portFormat, "register a port to open",
3975+ "The port will only be open while the service is exposed.",
3976+}
3977+
3978+func NewOpenPortCommand(ctx *ClientContext) (cmd.Command, error) {
3979+ if err := ctx.check(); err != nil {
3980+ return nil, err
3981+ }
3982+ return &portCommand{
3983+ ClientContext: ctx,
3984+ info: openPortInfo,
3985+ action: (*state.Unit).OpenPort,
3986+ }, nil
3987+}
3988+
3989+var closePortInfo = &cmd.Info{
3990+ "close-port", portFormat, "ensure a port is always closed", "",
3991+}
3992+
3993+func NewClosePortCommand(ctx *ClientContext) (cmd.Command, error) {
3994+ if err := ctx.check(); err != nil {
3995+ return nil, err
3996+ }
3997+ return &portCommand{
3998+ ClientContext: ctx,
3999+ info: closePortInfo,
4000+ action: (*state.Unit).ClosePort,
4001+ }, nil
4002+}
4003
4004=== added file 'cmd/jujuc/server/ports_test.go.OTHER'
4005--- cmd/jujuc/server/ports_test.go.OTHER 1970-01-01 00:00:00 +0000
4006+++ cmd/jujuc/server/ports_test.go.OTHER 2012-06-07 11:57:18 +0000
4007@@ -0,0 +1,86 @@
4008+package server_test
4009+
4010+import (
4011+ . "launchpad.net/gocheck"
4012+ "launchpad.net/juju-core/juju/cmd"
4013+ "launchpad.net/juju-core/juju/state"
4014+)
4015+
4016+type PortsSuite struct {
4017+ UnitFixture
4018+}
4019+
4020+var _ = Suite(&PortsSuite{})
4021+
4022+var portsTests = []struct {
4023+ cmd []string
4024+ open []state.Port
4025+}{
4026+ {[]string{"open-port", "80"}, []state.Port{{"tcp", 80}}},
4027+ {[]string{"open-port", "99/tcp"}, []state.Port{{"tcp", 80}, {"tcp", 99}}},
4028+ {[]string{"close-port", "80/TCP"}, []state.Port{{"tcp", 99}}},
4029+ {[]string{"open-port", "123/udp"}, []state.Port{{"tcp", 99}, {"udp", 123}}},
4030+ {[]string{"close-port", "9999/UDP"}, []state.Port{{"tcp", 99}, {"udp", 123}}},
4031+}
4032+
4033+func (s *PortsSuite) TestOpenClose(c *C) {
4034+ for _, t := range portsTests {
4035+ com, err := s.ctx.NewCommand(t.cmd[0])
4036+ c.Assert(err, IsNil)
4037+ ctx := dummyContext(c)
4038+ code := cmd.Main(com, ctx, t.cmd[1:])
4039+ c.Assert(code, Equals, 0)
4040+ c.Assert(bufferString(ctx.Stdout), Equals, "")
4041+ c.Assert(bufferString(ctx.Stderr), Equals, "")
4042+ open, err := s.unit.OpenPorts()
4043+ c.Assert(err, IsNil)
4044+ c.Assert(open, DeepEquals, t.open)
4045+ }
4046+}
4047+
4048+var badPortsTests = []struct {
4049+ args []string
4050+ err string
4051+}{
4052+ {nil, "no port specified"},
4053+ {[]string{"0"}, `port must be in the range \[1, 65535\]; got "0"`},
4054+ {[]string{"65536"}, `port must be in the range \[1, 65535\]; got "65536"`},
4055+ {[]string{"two"}, `port must be in the range \[1, 65535\]; got "two"`},
4056+ {[]string{"80/http"}, `protocol must be "tcp" or "udp"; got "http"`},
4057+ {[]string{"blah/blah/blah"}, `expected <port>\[/<protocol>\]; got "blah/blah/blah"`},
4058+ {[]string{"123", "haha"}, `unrecognized args: \["haha"\]`},
4059+}
4060+
4061+func (s *PortsSuite) TestBadArgs(c *C) {
4062+ for _, name := range []string{"open-port", "close-port"} {
4063+ for _, t := range badPortsTests {
4064+ com, err := s.ctx.NewCommand(name)
4065+ c.Assert(err, IsNil)
4066+ err = com.Init(dummyFlagSet(), t.args)
4067+ c.Assert(err, ErrorMatches, t.err)
4068+ }
4069+ }
4070+}
4071+
4072+func (s *PortsSuite) TestHelp(c *C) {
4073+ open, err := s.ctx.NewCommand("open-port")
4074+ c.Assert(err, IsNil)
4075+ c.Assert(string(open.Info().Help(dummyFlagSet())), Equals, `
4076+usage: open-port <port>[/<protocol>]
4077+purpose: register a port to open
4078+
4079+The port will only be open while the service is exposed.
4080+`[1:])
4081+
4082+ close, err := s.ctx.NewCommand("close-port")
4083+ c.Assert(err, IsNil)
4084+ c.Assert(string(close.Info().Help(dummyFlagSet())), Equals, `
4085+usage: close-port <port>[/<protocol>]
4086+purpose: ensure a port is always closed
4087+`[1:])
4088+}
4089+
4090+func (s *PortsSuite) TestUnitCommands(c *C) {
4091+ s.AssertUnitCommand(c, "open-port")
4092+ s.AssertUnitCommand(c, "close-port")
4093+}
4094
4095=== added file 'cmd/jujuc/server/server.go.OTHER'
4096--- cmd/jujuc/server/server.go.OTHER 1970-01-01 00:00:00 +0000
4097+++ cmd/jujuc/server/server.go.OTHER 2012-06-07 11:57:18 +0000
4098@@ -0,0 +1,133 @@
4099+// The cmd/jujuc/server package allows a process to expose an RPC interface that
4100+// allows client processes to delegate execution of cmd.Commands to a server
4101+// process (with the exposed commands amenable to specialisation by context id).
4102+package server
4103+
4104+import (
4105+ "bytes"
4106+ "fmt"
4107+ "launchpad.net/juju-core/juju/cmd"
4108+ "net"
4109+ "net/rpc"
4110+ "os"
4111+ "path/filepath"
4112+ "sync"
4113+)
4114+
4115+// Request contains the information necessary to run a Command remotely.
4116+type Request struct {
4117+ ContextId string
4118+ Dir string
4119+ CommandName string
4120+ Args []string
4121+}
4122+
4123+// Response contains the return code and output generated by a Request.
4124+type Response struct {
4125+ Code int
4126+ Stdout []byte
4127+ Stderr []byte
4128+}
4129+
4130+// CmdGetter looks up a Command implementation connected to a particular Context.
4131+type CmdGetter func(contextId, cmdName string) (cmd.Command, error)
4132+
4133+// Jujuc implements the jujuc command in the form required by net/rpc.
4134+type Jujuc struct {
4135+ getCmd CmdGetter
4136+}
4137+
4138+// badReqErr returns an error indicating a bad Request.
4139+func badReqErr(format string, v ...interface{}) error {
4140+ return fmt.Errorf("bad request: "+format, v...)
4141+}
4142+
4143+// Main runs the Command specified by req, and fills in resp.
4144+func (j *Jujuc) Main(req Request, resp *Response) error {
4145+ if req.CommandName == "" {
4146+ return badReqErr("command not specified")
4147+ }
4148+ if !filepath.IsAbs(req.Dir) {
4149+ return badReqErr("Dir is not absolute")
4150+ }
4151+ c, err := j.getCmd(req.ContextId, req.CommandName)
4152+ if err != nil {
4153+ return badReqErr("%s", err)
4154+ }
4155+ var stdout, stderr bytes.Buffer
4156+ ctx := &cmd.Context{req.Dir, &stdout, &stderr}
4157+ resp.Code = cmd.Main(c, ctx, req.Args)
4158+ resp.Stdout = stdout.Bytes()
4159+ resp.Stderr = stderr.Bytes()
4160+ return nil
4161+}
4162+
4163+// Server implements a server that serves command invocations via
4164+// a unix domain socket.
4165+type Server struct {
4166+ socketPath string
4167+ listener net.Listener
4168+ server *rpc.Server
4169+ closed chan bool
4170+ closing chan bool
4171+ wg sync.WaitGroup
4172+}
4173+
4174+// NewServer creates an RPC server bound to socketPath, which can execute
4175+// remote command invocations against an appropriate Context. It will not
4176+// actually do so until Run is called.
4177+func NewServer(getCmd CmdGetter, socketPath string) (*Server, error) {
4178+ server := rpc.NewServer()
4179+ if err := server.Register(&Jujuc{getCmd}); err != nil {
4180+ return nil, err
4181+ }
4182+ listener, err := net.Listen("unix", socketPath)
4183+ if err != nil {
4184+ return nil, err
4185+ }
4186+ s := &Server{
4187+ socketPath: socketPath,
4188+ listener: listener,
4189+ server: server,
4190+ closed: make(chan bool),
4191+ closing: make(chan bool),
4192+ }
4193+ return s, nil
4194+}
4195+
4196+// Run accepts new connections until it encounters an error, or until Close is
4197+// called, and then blocks until all existing connections have been closed.
4198+func (s *Server) Run() (err error) {
4199+ var conn net.Conn
4200+ for {
4201+ conn, err = s.listener.Accept()
4202+ if err != nil {
4203+ break
4204+ }
4205+ s.wg.Add(1)
4206+ go func(conn net.Conn) {
4207+ s.server.ServeConn(conn)
4208+ s.wg.Done()
4209+ }(conn)
4210+ }
4211+ select {
4212+ case <-s.closing:
4213+ // Someone has called Close(), so it is overwhelmingly likely that
4214+ // the error from Accept is a direct result of the Listener being
4215+ // closed, and can therefore be safely ignored.
4216+ err = nil
4217+ default:
4218+ }
4219+ s.wg.Wait()
4220+ close(s.closed)
4221+ return
4222+}
4223+
4224+// Close immediately stops accepting connections, and blocks until all existing
4225+// connections have been closed.
4226+func (s *Server) Close() {
4227+ close(s.closing)
4228+ s.listener.Close()
4229+ os.Remove(s.socketPath)
4230+ <-s.closed
4231+}
4232
4233=== added file 'cmd/jujuc/server/server_test.go.OTHER'
4234--- cmd/jujuc/server/server_test.go.OTHER 1970-01-01 00:00:00 +0000
4235+++ cmd/jujuc/server/server_test.go.OTHER 2012-06-07 11:57:18 +0000
4236@@ -0,0 +1,146 @@
4237+package server_test
4238+
4239+import (
4240+ "errors"
4241+ "fmt"
4242+ "io/ioutil"
4243+ "launchpad.net/gnuflag"
4244+ . "launchpad.net/gocheck"
4245+ "launchpad.net/juju-core/juju/cmd"
4246+ "launchpad.net/juju-core/juju/cmd/jujuc/server"
4247+ "net/rpc"
4248+ "os"
4249+ "path/filepath"
4250+)
4251+
4252+type RpcCommand struct {
4253+ Value string
4254+}
4255+
4256+func (c *RpcCommand) Info() *cmd.Info {
4257+ return &cmd.Info{"remote", "", "act at a distance", "blah doc"}
4258+}
4259+
4260+func (c *RpcCommand) Init(f *gnuflag.FlagSet, args []string) error {
4261+ f.StringVar(&c.Value, "value", "", "doc")
4262+ if err := f.Parse(true, args); err != nil {
4263+ return err
4264+ }
4265+ return cmd.CheckEmpty(f.Args())
4266+}
4267+
4268+func (c *RpcCommand) Run(ctx *cmd.Context) error {
4269+ if c.Value == "error" {
4270+ return errors.New("blam")
4271+ }
4272+ ctx.Stdout.Write([]byte("eye of newt\n"))
4273+ ctx.Stderr.Write([]byte("toe of frog\n"))
4274+ return ioutil.WriteFile(ctx.AbsPath("local"), []byte(c.Value), 0644)
4275+}
4276+
4277+func factory(contextId, cmdName string) (cmd.Command, error) {
4278+ if contextId != "validCtx" {
4279+ return nil, fmt.Errorf("unknown context %q", contextId)
4280+ }
4281+ if cmdName != "remote" {
4282+ return nil, fmt.Errorf("unknown command %q", cmdName)
4283+ }
4284+ return &RpcCommand{}, nil
4285+}
4286+
4287+type ServerSuite struct {
4288+ server *server.Server
4289+ sockPath string
4290+ err chan error
4291+}
4292+
4293+var _ = Suite(&ServerSuite{})
4294+
4295+func (s *ServerSuite) SetUpTest(c *C) {
4296+ s.sockPath = filepath.Join(c.MkDir(), "test.sock")
4297+ srv, err := server.NewServer(factory, s.sockPath)
4298+ c.Assert(err, IsNil)
4299+ c.Assert(srv, NotNil)
4300+ s.server = srv
4301+ s.err = make(chan error)
4302+ go func() { s.err <- s.server.Run() }()
4303+}
4304+
4305+func (s *ServerSuite) TearDownTest(c *C) {
4306+ s.server.Close()
4307+ c.Assert(<-s.err, IsNil)
4308+ _, err := os.Open(s.sockPath)
4309+ c.Assert(os.IsNotExist(err), Equals, true)
4310+}
4311+
4312+func (s *ServerSuite) Call(c *C, req server.Request) (resp server.Response, err error) {
4313+ client, err := rpc.Dial("unix", s.sockPath)
4314+ c.Assert(err, IsNil)
4315+ defer client.Close()
4316+ err = client.Call("Jujuc.Main", req, &resp)
4317+ return resp, err
4318+}
4319+
4320+func (s *ServerSuite) TestHappyPath(c *C) {
4321+ dir := c.MkDir()
4322+ resp, err := s.Call(c, server.Request{
4323+ "validCtx", dir, "remote", []string{"--value", "something"}})
4324+ c.Assert(err, IsNil)
4325+ c.Assert(resp.Code, Equals, 0)
4326+ c.Assert(string(resp.Stdout), Equals, "eye of newt\n")
4327+ c.Assert(string(resp.Stderr), Equals, "toe of frog\n")
4328+ content, err := ioutil.ReadFile(filepath.Join(dir, "local"))
4329+ c.Assert(err, IsNil)
4330+ c.Assert(string(content), Equals, "something")
4331+}
4332+
4333+func (s *ServerSuite) TestBadCommandName(c *C) {
4334+ dir := c.MkDir()
4335+ _, err := s.Call(c, server.Request{"validCtx", dir, "", nil})
4336+ c.Assert(err, ErrorMatches, "bad request: command not specified")
4337+ _, err = s.Call(c, server.Request{"validCtx", dir, "witchcraft", nil})
4338+ c.Assert(err, ErrorMatches, `bad request: unknown command "witchcraft"`)
4339+}
4340+
4341+func (s *ServerSuite) TestBadDir(c *C) {
4342+ for _, req := range []server.Request{
4343+ {"validCtx", "", "anything", nil},
4344+ {"validCtx", "foo/bar", "anything", nil},
4345+ } {
4346+ _, err := s.Call(c, req)
4347+ c.Assert(err, ErrorMatches, "bad request: Dir is not absolute")
4348+ }
4349+}
4350+
4351+func (s *ServerSuite) TestBadContextId(c *C) {
4352+ _, err := s.Call(c, server.Request{"whatever", c.MkDir(), "remote", nil})
4353+ c.Assert(err, ErrorMatches, `bad request: unknown context "whatever"`)
4354+}
4355+
4356+func (s *ServerSuite) AssertBadCommand(c *C, args []string, code int) server.Response {
4357+ resp, err := s.Call(c, server.Request{"validCtx", c.MkDir(), args[0], args[1:]})
4358+ c.Assert(err, IsNil)
4359+ c.Assert(resp.Code, Equals, code)
4360+ return resp
4361+}
4362+
4363+func (s *ServerSuite) TestParseError(c *C) {
4364+ resp := s.AssertBadCommand(c, []string{"remote", "--cheese"}, 2)
4365+ c.Assert(string(resp.Stdout), Equals, "")
4366+ c.Assert(string(resp.Stderr), Equals, `usage: remote [options]
4367+purpose: act at a distance
4368+
4369+options:
4370+--value (= "")
4371+ doc
4372+
4373+blah doc
4374+error: flag provided but not defined: --cheese
4375+`)
4376+}
4377+
4378+func (s *ServerSuite) TestBrokenCommand(c *C) {
4379+ resp := s.AssertBadCommand(c, []string{"remote", "--value", "error"}, 1)
4380+ c.Assert(string(resp.Stdout), Equals, "")
4381+ c.Assert(string(resp.Stderr), Equals, "error: blam\n")
4382+}
4383
4384=== added file 'cmd/jujuc/server/unit-get.go.OTHER'
4385--- cmd/jujuc/server/unit-get.go.OTHER 1970-01-01 00:00:00 +0000
4386+++ cmd/jujuc/server/unit-get.go.OTHER 2012-06-07 11:57:18 +0000
4387@@ -0,0 +1,66 @@
4388+package server
4389+
4390+import (
4391+ "errors"
4392+ "fmt"
4393+ "launchpad.net/gnuflag"
4394+ "launchpad.net/juju-core/juju/cmd"
4395+ "launchpad.net/juju-core/juju/state"
4396+)
4397+
4398+// UnitGetCommand implements the unit-get command.
4399+type UnitGetCommand struct {
4400+ *ClientContext
4401+ Key string
4402+ out output
4403+}
4404+
4405+func NewUnitGetCommand(ctx *ClientContext) (cmd.Command, error) {
4406+ if err := ctx.check(); err != nil {
4407+ return nil, err
4408+ }
4409+ return &UnitGetCommand{ClientContext: ctx}, nil
4410+}
4411+
4412+func (c *UnitGetCommand) Info() *cmd.Info {
4413+ return &cmd.Info{
4414+ "unit-get", "<setting>", "print public-address or private-address", "",
4415+ }
4416+}
4417+
4418+func (c *UnitGetCommand) Init(f *gnuflag.FlagSet, args []string) error {
4419+ c.out.addFlags(f, "yaml", defaultFormatters)
4420+ if err := f.Parse(true, args); err != nil {
4421+ return err
4422+ }
4423+ args = f.Args()
4424+ if args == nil {
4425+ return errors.New("no setting specified")
4426+ }
4427+ if args[0] != "private-address" && args[0] != "public-address" {
4428+ return fmt.Errorf("unknown setting %q", args[0])
4429+ }
4430+ c.Key = args[0]
4431+ return cmd.CheckEmpty(args[1:])
4432+}
4433+
4434+func (c *UnitGetCommand) Run(ctx *cmd.Context) (err error) {
4435+ var unit *state.Unit
4436+ unit, err = c.State.Unit(c.LocalUnitName)
4437+ if err != nil {
4438+ return
4439+ }
4440+ var value string
4441+ if c.Key == "private-address" {
4442+ value, err = unit.PrivateAddress()
4443+ } else {
4444+ value, err = unit.PublicAddress()
4445+ }
4446+ if err != nil {
4447+ return
4448+ }
4449+ if c.out.testMode {
4450+ return truthError(value)
4451+ }
4452+ return c.out.write(ctx, value)
4453+}
4454
4455=== added file 'cmd/jujuc/server/unit-get_test.go.OTHER'
4456--- cmd/jujuc/server/unit-get_test.go.OTHER 1970-01-01 00:00:00 +0000
4457+++ cmd/jujuc/server/unit-get_test.go.OTHER 2012-06-07 11:57:18 +0000
4458@@ -0,0 +1,109 @@
4459+package server_test
4460+
4461+import (
4462+ "io/ioutil"
4463+ . "launchpad.net/gocheck"
4464+ "launchpad.net/juju-core/juju/cmd"
4465+ "path/filepath"
4466+)
4467+
4468+type UnitGetSuite struct {
4469+ UnitFixture
4470+}
4471+
4472+var _ = Suite(&UnitGetSuite{})
4473+
4474+func (s *UnitGetSuite) SetUpTest(c *C) {
4475+ s.UnitFixture.SetUpTest(c)
4476+ err := s.unit.SetPublicAddress("gimli.minecraft.example.com")
4477+ c.Assert(err, IsNil)
4478+ err = s.unit.SetPrivateAddress("192.168.0.99")
4479+ c.Assert(err, IsNil)
4480+}
4481+
4482+var unitGetTests = []struct {
4483+ args []string
4484+ out string
4485+}{
4486+ {[]string{"private-address"}, "192.168.0.99\n\n"},
4487+ {[]string{"private-address", "--format", "yaml"}, "192.168.0.99\n\n"},
4488+ {[]string{"private-address", "--format", "json"}, `"192.168.0.99"` + "\n"},
4489+ {[]string{"public-address"}, "gimli.minecraft.example.com\n\n"},
4490+ {[]string{"public-address", "--format", "yaml"}, "gimli.minecraft.example.com\n\n"},
4491+ {[]string{"public-address", "--format", "json"}, `"gimli.minecraft.example.com"` + "\n"},
4492+}
4493+
4494+func (s *UnitGetSuite) TestOutputFormat(c *C) {
4495+ for _, t := range unitGetTests {
4496+ com, err := s.ctx.NewCommand("unit-get")
4497+ c.Assert(err, IsNil)
4498+ ctx := dummyContext(c)
4499+ code := cmd.Main(com, ctx, t.args)
4500+ c.Assert(code, Equals, 0)
4501+ c.Assert(bufferString(ctx.Stderr), Equals, "")
4502+ c.Assert(bufferString(ctx.Stdout), Matches, t.out)
4503+ }
4504+}
4505+
4506+func (s *UnitGetSuite) TestTestMode(c *C) {
4507+ for _, key := range []string{"public-address", "private-address"} {
4508+ com, err := s.ctx.NewCommand("unit-get")
4509+ c.Assert(err, IsNil)
4510+ ctx := dummyContext(c)
4511+ code := cmd.Main(com, ctx, []string{"--test", key})
4512+ c.Assert(code, Equals, 0)
4513+ c.Assert(bufferString(ctx.Stderr), Equals, "")
4514+ c.Assert(bufferString(ctx.Stdout), Equals, "")
4515+ }
4516+}
4517+
4518+func (s *UnitGetSuite) TestHelp(c *C) {
4519+ com, err := s.ctx.NewCommand("unit-get")
4520+ c.Assert(err, IsNil)
4521+ ctx := dummyContext(c)
4522+ code := cmd.Main(com, ctx, []string{"--help"})
4523+ c.Assert(code, Equals, 0)
4524+ c.Assert(bufferString(ctx.Stdout), Equals, "")
4525+ c.Assert(bufferString(ctx.Stderr), Equals, `usage: unit-get [options] <setting>
4526+purpose: print public-address or private-address
4527+
4528+options:
4529+--format (= yaml)
4530+ specify output format (json|yaml)
4531+-o, --output (= "")
4532+ specify an output file
4533+--test (= false)
4534+ returns non-zero exit code if value is false/zero/empty
4535+`)
4536+}
4537+
4538+func (s *UnitGetSuite) TestOutputPath(c *C) {
4539+ com, err := s.ctx.NewCommand("unit-get")
4540+ c.Assert(err, IsNil)
4541+ ctx := dummyContext(c)
4542+ code := cmd.Main(com, ctx, []string{"--output", "some-file", "private-address"})
4543+ c.Assert(code, Equals, 0)
4544+ c.Assert(bufferString(ctx.Stderr), Equals, "")
4545+ c.Assert(bufferString(ctx.Stdout), Equals, "")
4546+ content, err := ioutil.ReadFile(filepath.Join(ctx.Dir, "some-file"))
4547+ c.Assert(err, IsNil)
4548+ c.Assert(string(content), Equals, "192.168.0.99\n\n")
4549+}
4550+
4551+func (s *UnitGetSuite) TestUnknownSetting(c *C) {
4552+ com, err := s.ctx.NewCommand("unit-get")
4553+ c.Assert(err, IsNil)
4554+ err = com.Init(dummyFlagSet(), []string{"protected-address"})
4555+ c.Assert(err, ErrorMatches, `unknown setting "protected-address"`)
4556+}
4557+
4558+func (s *UnitGetSuite) TestUnknownArg(c *C) {
4559+ com, err := s.ctx.NewCommand("unit-get")
4560+ c.Assert(err, IsNil)
4561+ err = com.Init(dummyFlagSet(), []string{"private-address", "blah"})
4562+ c.Assert(err, ErrorMatches, `unrecognized args: \["blah"\]`)
4563+}
4564+
4565+func (s *UnitGetSuite) TestUnitCommand(c *C) {
4566+ s.AssertUnitCommand(c, "unit-get")
4567+}
4568
4569=== added file 'cmd/jujuc/server/util_test.go.OTHER'
4570--- cmd/jujuc/server/util_test.go.OTHER 1970-01-01 00:00:00 +0000
4571+++ cmd/jujuc/server/util_test.go.OTHER 2012-06-07 11:57:18 +0000
4572@@ -0,0 +1,149 @@
4573+package server_test
4574+
4575+import (
4576+ "bytes"
4577+ "fmt"
4578+ "io"
4579+ . "launchpad.net/gocheck"
4580+ "launchpad.net/gozk/zookeeper"
4581+ "launchpad.net/juju-core/juju/charm"
4582+ "launchpad.net/juju-core/juju/cmd"
4583+ "launchpad.net/juju-core/juju/cmd/jujuc/server"
4584+ "launchpad.net/juju-core/juju/state"
4585+ "launchpad.net/juju-core/juju/testing"
4586+ "net/url"
4587+ stdtesting "testing"
4588+)
4589+
4590+var zkAddr string
4591+
4592+func TestPackage(t *stdtesting.T) {
4593+ srv := testing.StartZkServer()
4594+ defer srv.Destroy()
4595+ var err error
4596+ zkAddr, err = srv.Addr()
4597+ if err != nil {
4598+ t.Fatalf("could not get ZooKeeper server address")
4599+ }
4600+ TestingT(t)
4601+}
4602+
4603+func addDummyCharm(c *C, st *state.State) *state.Charm {
4604+ ch := testing.Charms.Dir("dummy")
4605+ u := fmt.Sprintf("local:series/%s-%d", ch.Meta().Name, ch.Revision())
4606+ curl := charm.MustParseURL(u)
4607+ burl, err := url.Parse("http://bundle.url")
4608+ c.Assert(err, IsNil)
4609+ dummy, err := st.AddCharm(ch, curl, burl, "dummy-sha256")
4610+ c.Assert(err, IsNil)
4611+ return dummy
4612+}
4613+
4614+func dummyContext(c *C) *cmd.Context {
4615+ return &cmd.Context{c.MkDir(), &bytes.Buffer{}, &bytes.Buffer{}}
4616+}
4617+
4618+func bufferString(w io.Writer) string {
4619+ return w.(*bytes.Buffer).String()
4620+}
4621+
4622+type UnitFixture struct {
4623+ ctx *server.ClientContext
4624+ service *state.Service
4625+ unit *state.Unit
4626+}
4627+
4628+func (f *UnitFixture) SetUpTest(c *C) {
4629+ st, err := state.Initialize(&state.Info{
4630+ Addrs: []string{zkAddr},
4631+ })
4632+ c.Assert(err, IsNil)
4633+ f.ctx = &server.ClientContext{
4634+ Id: "TestCtx",
4635+ State: st,
4636+ LocalUnitName: "minecraft/0",
4637+ }
4638+ dummy := addDummyCharm(c, st)
4639+ f.service, err = st.AddService("minecraft", dummy)
4640+ c.Assert(err, IsNil)
4641+ f.unit, err = f.service.AddUnit()
4642+ c.Assert(err, IsNil)
4643+}
4644+
4645+func (f *UnitFixture) TearDownTest(c *C) {
4646+ zk, session, err := zookeeper.Dial(zkAddr, 15e9)
4647+ c.Assert(err, IsNil)
4648+ event := <-session
4649+ c.Assert(event.Ok(), Equals, true)
4650+ c.Assert(event.Type, Equals, zookeeper.EVENT_SESSION)
4651+ c.Assert(event.State, Equals, zookeeper.STATE_CONNECTED)
4652+ testing.ZkRemoveTree(zk, "/")
4653+}
4654+
4655+func (f *UnitFixture) AssertUnitCommand(c *C, name string) {
4656+ ctx := &server.ClientContext{Id: "TestCtx", State: f.ctx.State}
4657+ com, err := ctx.NewCommand(name)
4658+ c.Assert(com, IsNil)
4659+ c.Assert(err, ErrorMatches, "context TestCtx is not attached to a unit")
4660+
4661+ ctx = &server.ClientContext{Id: "TestCtx", LocalUnitName: f.ctx.LocalUnitName}
4662+ com, err = ctx.NewCommand(name)
4663+ c.Assert(com, IsNil)
4664+ c.Assert(err, ErrorMatches, "context TestCtx cannot access state")
4665+}
4666+
4667+type TruthErrorSuite struct{}
4668+
4669+var _ = Suite(&TruthErrorSuite{})
4670+
4671+var truthErrorTests = []struct {
4672+ value interface{}
4673+ err error
4674+}{
4675+ {0, cmd.ErrSilent},
4676+ {int8(0), cmd.ErrSilent},
4677+ {int16(0), cmd.ErrSilent},
4678+ {int32(0), cmd.ErrSilent},
4679+ {int64(0), cmd.ErrSilent},
4680+ {uint(0), cmd.ErrSilent},
4681+ {uint8(0), cmd.ErrSilent},
4682+ {uint16(0), cmd.ErrSilent},
4683+ {uint32(0), cmd.ErrSilent},
4684+ {uint64(0), cmd.ErrSilent},
4685+ {uintptr(0), cmd.ErrSilent},
4686+ {123, nil},
4687+ {int8(123), nil},
4688+ {int16(123), nil},
4689+ {int32(123), nil},
4690+ {int64(123), nil},
4691+ {uint(123), nil},
4692+ {uint8(123), nil},
4693+ {uint16(123), nil},
4694+ {uint32(123), nil},
4695+ {uint64(123), nil},
4696+ {uintptr(123), nil},
4697+ {0.0, cmd.ErrSilent},
4698+ {float32(0.0), cmd.ErrSilent},
4699+ {123.45, nil},
4700+ {float32(123.45), nil},
4701+ {nil, cmd.ErrSilent},
4702+ {"", cmd.ErrSilent},
4703+ {"blah", nil},
4704+ {true, nil},
4705+ {false, cmd.ErrSilent},
4706+ {[]string{}, cmd.ErrSilent},
4707+ {[]string{""}, nil},
4708+ {[]bool{}, cmd.ErrSilent},
4709+ {[]bool{false}, nil},
4710+ {map[string]string{}, cmd.ErrSilent},
4711+ {map[string]string{"": ""}, nil},
4712+ {map[bool]bool{}, cmd.ErrSilent},
4713+ {map[bool]bool{false: false}, nil},
4714+ {struct{ x bool }{false}, nil},
4715+}
4716+
4717+func (s *TruthErrorSuite) TestTruthError(c *C) {
4718+ for _, t := range truthErrorTests {
4719+ c.Assert(server.TruthError(t.value), Equals, t.err)
4720+ }
4721+}
4722
4723=== added directory 'cmd/jujud'
4724=== added file 'cmd/jujud/agent.go.OTHER'
4725--- cmd/jujud/agent.go.OTHER 1970-01-01 00:00:00 +0000
4726+++ cmd/jujud/agent.go.OTHER 2012-06-07 11:57:18 +0000
4727@@ -0,0 +1,70 @@
4728+package main
4729+
4730+import (
4731+ "fmt"
4732+ "launchpad.net/gnuflag"
4733+ "launchpad.net/juju-core/juju/cmd"
4734+ "launchpad.net/juju-core/juju/state"
4735+ "regexp"
4736+ "strings"
4737+)
4738+
4739+// requiredError is useful when complaining about missing command-line options.
4740+func requiredError(name string) error {
4741+ return fmt.Errorf("--%s option must be set", name)
4742+}
4743+
4744+// stateInfoValue implements gnuflag.Value on a state.Info.
4745+type stateInfoValue state.Info
4746+
4747+var validAddr = regexp.MustCompile("^.+:[0-9]+$")
4748+
4749+// Set splits the comma-separated list of ZooKeeper addresses and stores
4750+// onto v's Addrs. Addresses must include port numbers.
4751+func (v *stateInfoValue) Set(value string) error {
4752+ addrs := strings.Split(value, ",")
4753+ for _, addr := range addrs {
4754+ if !validAddr.MatchString(addr) {
4755+ return fmt.Errorf("%q is not a valid zookeeper address", addr)
4756+ }
4757+ }
4758+ v.Addrs = addrs
4759+ return nil
4760+}
4761+
4762+// String returns the list of ZooKeeper addresses joined by commas.
4763+func (v *stateInfoValue) String() string {
4764+ if v.Addrs != nil {
4765+ return strings.Join(v.Addrs, ",")
4766+ }
4767+ return ""
4768+}
4769+
4770+// stateInfoVar sets up a gnuflag flag analagously to FlagSet.*Var methods.
4771+func stateInfoVar(fs *gnuflag.FlagSet, target *state.Info, name string, value []string, usage string) {
4772+ target.Addrs = value
4773+ fs.Var((*stateInfoValue)(target), name, usage)
4774+}
4775+
4776+// AgentConf handles command-line flags shared by all agents.
4777+type AgentConf struct {
4778+ JujuDir string // Defaults to "/var/lib/juju".
4779+ StateInfo state.Info
4780+}
4781+
4782+// addFlags injects common agent flags into f.
4783+func (c *AgentConf) addFlags(f *gnuflag.FlagSet) {
4784+ f.StringVar(&c.JujuDir, "juju-directory", "/var/lib/juju", "juju working directory")
4785+ stateInfoVar(f, &c.StateInfo, "zookeeper-servers", nil, "zookeeper servers to connect to")
4786+}
4787+
4788+// checkArgs checks that required flags have been set and that args is empty.
4789+func (c *AgentConf) checkArgs(args []string) error {
4790+ if c.JujuDir == "" {
4791+ return requiredError("juju-directory")
4792+ }
4793+ if c.StateInfo.Addrs == nil {
4794+ return requiredError("zookeeper-servers")
4795+ }
4796+ return cmd.CheckEmpty(args)
4797+}
4798
4799=== added file 'cmd/jujud/initzk.go.OTHER'
4800--- cmd/jujud/initzk.go.OTHER 1970-01-01 00:00:00 +0000
4801+++ cmd/jujud/initzk.go.OTHER 2012-06-07 11:57:18 +0000
4802@@ -0,0 +1,44 @@
4803+package main
4804+
4805+import (
4806+ "launchpad.net/gnuflag"
4807+ "launchpad.net/juju-core/juju/cmd"
4808+ "launchpad.net/juju-core/juju/state"
4809+)
4810+
4811+type InitzkCommand struct {
4812+ StateInfo state.Info
4813+ InstanceId string
4814+ EnvType string
4815+}
4816+
4817+// Info returns a decription of the command.
4818+func (c *InitzkCommand) Info() *cmd.Info {
4819+ return &cmd.Info{"initzk", "", "initialize juju state in a local zookeeper", ""}
4820+}
4821+
4822+// Init initializes the command for running.
4823+func (c *InitzkCommand) Init(f *gnuflag.FlagSet, args []string) error {
4824+ stateInfoVar(f, &c.StateInfo, "zookeeper-servers", []string{"127.0.0.1:2181"}, "address of zookeeper to initialize")
4825+ f.StringVar(&c.InstanceId, "instance-id", "", "instance id of this machine")
4826+ f.StringVar(&c.EnvType, "env-type", "", "environment type")
4827+ if err := f.Parse(true, args); err != nil {
4828+ return err
4829+ }
4830+ if c.StateInfo.Addrs == nil {
4831+ return requiredError("zookeeper-servers")
4832+ }
4833+ if c.InstanceId == "" {
4834+ return requiredError("instance-id")
4835+ }
4836+ if c.EnvType == "" {
4837+ return requiredError("env-type")
4838+ }
4839+ return cmd.CheckEmpty(f.Args())
4840+}
4841+
4842+// Run initializes zookeeper state for an environment.
4843+func (c *InitzkCommand) Run(_ *cmd.Context) error {
4844+ _, err := state.Initialize(&c.StateInfo)
4845+ return err
4846+}
4847
4848=== added file 'cmd/jujud/initzk_test.go.OTHER'
4849--- cmd/jujud/initzk_test.go.OTHER 1970-01-01 00:00:00 +0000
4850+++ cmd/jujud/initzk_test.go.OTHER 2012-06-07 11:57:18 +0000
4851@@ -0,0 +1,78 @@
4852+package main
4853+
4854+import (
4855+ . "launchpad.net/gocheck"
4856+ "launchpad.net/gozk/zookeeper"
4857+ "launchpad.net/juju-core/juju/testing"
4858+ stdtesting "testing"
4859+)
4860+
4861+var zkAddr string
4862+
4863+func TestPackage(t *stdtesting.T) {
4864+ srv := testing.StartZkServer()
4865+ defer srv.Destroy()
4866+ var err error
4867+ zkAddr, err = srv.Addr()
4868+ if err != nil {
4869+ t.Fatalf("could not get ZooKeeper server address")
4870+ }
4871+ TestingT(t)
4872+}
4873+
4874+type InitzkSuite struct {
4875+ zkConn *zookeeper.Conn
4876+ path string
4877+}
4878+
4879+var _ = Suite(&InitzkSuite{})
4880+
4881+func (s *InitzkSuite) SetUpTest(c *C) {
4882+ zk, session, err := zookeeper.Dial(zkAddr, 15e9)
4883+ c.Assert(err, IsNil)
4884+ event := <-session
4885+ c.Assert(event.Ok(), Equals, true)
4886+ c.Assert(event.Type, Equals, zookeeper.EVENT_SESSION)
4887+ c.Assert(event.State, Equals, zookeeper.STATE_CONNECTED)
4888+
4889+ s.zkConn = zk
4890+ s.path = "/watcher"
4891+
4892+ c.Assert(err, IsNil)
4893+}
4894+
4895+func (s *InitzkSuite) TearDownTest(c *C) {
4896+ testing.ZkRemoveTree(s.zkConn, s.path)
4897+ s.zkConn.Close()
4898+}
4899+
4900+func initInitzkCommand(args []string) (*InitzkCommand, error) {
4901+ c := &InitzkCommand{}
4902+ return c, initCmd(c, args)
4903+}
4904+
4905+func (s *InitzkSuite) TestParse(c *C) {
4906+ args := []string{}
4907+ _, err := initInitzkCommand(args)
4908+ c.Assert(err, ErrorMatches, "--instance-id option must be set")
4909+
4910+ args = append(args, "--instance-id", "iWhatever")
4911+ _, err = initInitzkCommand(args)
4912+ c.Assert(err, ErrorMatches, "--env-type option must be set")
4913+
4914+ args = append(args, "--env-type", "dummy")
4915+ izk, err := initInitzkCommand(args)
4916+ c.Assert(err, IsNil)
4917+ c.Assert(izk.StateInfo.Addrs, DeepEquals, []string{"127.0.0.1:2181"})
4918+ c.Assert(izk.InstanceId, Equals, "iWhatever")
4919+ c.Assert(izk.EnvType, Equals, "dummy")
4920+
4921+ args = append(args, "--zookeeper-servers", "zk1:2181,zk2:2181")
4922+ izk, err = initInitzkCommand(args)
4923+ c.Assert(err, IsNil)
4924+ c.Assert(izk.StateInfo.Addrs, DeepEquals, []string{"zk1:2181", "zk2:2181"})
4925+
4926+ args = append(args, "haha disregard that")
4927+ _, err = initInitzkCommand(args)
4928+ c.Assert(err, ErrorMatches, `unrecognized args: \["haha disregard that"\]`)
4929+}
4930
4931=== added file 'cmd/jujud/machine.go.OTHER'
4932--- cmd/jujud/machine.go.OTHER 1970-01-01 00:00:00 +0000
4933+++ cmd/jujud/machine.go.OTHER 2012-06-07 11:57:18 +0000
4934@@ -0,0 +1,36 @@
4935+package main
4936+
4937+import (
4938+ "fmt"
4939+ "launchpad.net/gnuflag"
4940+ "launchpad.net/juju-core/juju/cmd"
4941+)
4942+
4943+// MachineAgent is a cmd.Command responsible for running a machine agent.
4944+type MachineAgent struct {
4945+ Conf AgentConf
4946+ MachineId int
4947+}
4948+
4949+// Info returns usage information for the command.
4950+func (a *MachineAgent) Info() *cmd.Info {
4951+ return &cmd.Info{"machine", "", "run a juju machine agent", ""}
4952+}
4953+
4954+// Init initializes the command for running.
4955+func (a *MachineAgent) Init(f *gnuflag.FlagSet, args []string) error {
4956+ a.Conf.addFlags(f)
4957+ f.IntVar(&a.MachineId, "machine-id", -1, "id of the machine to run")
4958+ if err := f.Parse(true, args); err != nil {
4959+ return err
4960+ }
4961+ if a.MachineId < 0 {
4962+ return fmt.Errorf("--machine-id option must be set, and expects a non-negative integer")
4963+ }
4964+ return a.Conf.checkArgs(f.Args())
4965+}
4966+
4967+// Run runs a machine agent.
4968+func (a *MachineAgent) Run(_ *cmd.Context) error {
4969+ return fmt.Errorf("MachineAgent.Run not implemented")
4970+}
4971
4972=== added file 'cmd/jujud/machine_test.go.OTHER'
4973--- cmd/jujud/machine_test.go.OTHER 1970-01-01 00:00:00 +0000
4974+++ cmd/jujud/machine_test.go.OTHER 2012-06-07 11:57:18 +0000
4975@@ -0,0 +1,35 @@
4976+package main
4977+
4978+import (
4979+ . "launchpad.net/gocheck"
4980+ "launchpad.net/juju-core/juju/cmd"
4981+)
4982+
4983+type MachineSuite struct{}
4984+
4985+var _ = Suite(&MachineSuite{})
4986+
4987+func (s *MachineSuite) TestParseSuccess(c *C) {
4988+ create := func() (cmd.Command, *AgentConf) {
4989+ a := &MachineAgent{}
4990+ return a, &a.Conf
4991+ }
4992+ a := CheckAgentCommand(c, create, []string{"--machine-id", "42"})
4993+ c.Assert(a.(*MachineAgent).MachineId, Equals, 42)
4994+}
4995+
4996+func (s *MachineSuite) TestParseNonsense(c *C) {
4997+ for _, args := range [][]string{
4998+ []string{},
4999+ []string{"--machine-id", "-4004"},
5000+ } {
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: