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
=== added file '.lbox.OTHER'
--- .lbox.OTHER 1970-01-01 00:00:00 +0000
+++ .lbox.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,1 @@
1propose -cr -for lp:juju-core/juju
02
=== added directory 'charm'
=== added file 'charm/bundle_test.go.OTHER'
--- charm/bundle_test.go.OTHER 1970-01-01 00:00:00 +0000
+++ charm/bundle_test.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,196 @@
1package charm_test
2
3import (
4 "bytes"
5 "fmt"
6 "io/ioutil"
7 . "launchpad.net/gocheck"
8 "launchpad.net/juju-core/juju/charm"
9 "launchpad.net/juju-core/juju/testing"
10 "os"
11 "os/exec"
12 "path/filepath"
13)
14
15type BundleSuite struct {
16 repo *testing.Repo
17 bundlePath string
18}
19
20var _ = Suite(&BundleSuite{})
21
22func (s *BundleSuite) SetUpSuite(c *C) {
23 s.bundlePath = testing.Charms.BundlePath(c.MkDir(), "dummy")
24}
25
26func (s *BundleSuite) TestReadBundle(c *C) {
27 bundle, err := charm.ReadBundle(s.bundlePath)
28 c.Assert(err, IsNil)
29 checkDummy(c, bundle, s.bundlePath)
30}
31
32func (s *BundleSuite) TestReadBundleWithoutConfig(c *C) {
33 path := testing.Charms.BundlePath(c.MkDir(), "varnish")
34 bundle, err := charm.ReadBundle(path)
35 c.Assert(err, IsNil)
36
37 // A lacking config.yaml file still causes a proper
38 // Config value to be returned.
39 c.Assert(bundle.Config().Options, HasLen, 0)
40}
41
42func (s *BundleSuite) TestReadBundleBytes(c *C) {
43 data, err := ioutil.ReadFile(s.bundlePath)
44 c.Assert(err, IsNil)
45
46 bundle, err := charm.ReadBundleBytes(data)
47 c.Assert(err, IsNil)
48 checkDummy(c, bundle, "")
49}
50
51func (s *BundleSuite) TestExpandTo(c *C) {
52 bundle, err := charm.ReadBundle(s.bundlePath)
53 c.Assert(err, IsNil)
54
55 path := filepath.Join(c.MkDir(), "charm")
56 err = bundle.ExpandTo(path)
57 c.Assert(err, IsNil)
58
59 dir, err := charm.ReadDir(path)
60 c.Assert(err, IsNil)
61 checkDummy(c, dir, path)
62}
63
64func (s *BundleSuite) TestBundleFileModes(c *C) {
65 // Apply subtler mode differences than can be expressed in Bazaar.
66 srcPath := testing.Charms.ClonedDirPath(c.MkDir(), "dummy")
67 modes := []struct {
68 path string
69 mode os.FileMode
70 }{
71 {"hooks/install", 0751},
72 {"empty", 0750},
73 {"src/hello.c", 0614},
74 }
75 for _, m := range modes {
76 err := os.Chmod(filepath.Join(srcPath, m.path), m.mode)
77 if err != nil {
78 panic(err)
79 }
80 }
81
82 // Bundle and extract the charm to a new directory.
83 dir, err := charm.ReadDir(srcPath)
84 c.Assert(err, IsNil)
85 buf := new(bytes.Buffer)
86 err = dir.BundleTo(buf)
87 c.Assert(err, IsNil)
88 bundle, err := charm.ReadBundleBytes(buf.Bytes())
89 c.Assert(err, IsNil)
90 path := c.MkDir()
91 err = bundle.ExpandTo(path)
92 c.Assert(err, IsNil)
93
94 // Check sensible file modes once round-tripped.
95 info, err := os.Stat(filepath.Join(path, "src", "hello.c"))
96 c.Assert(err, IsNil)
97 c.Assert(info.Mode()&0777, Equals, os.FileMode(0644))
98 c.Assert(info.Mode()&os.ModeType, Equals, os.FileMode(0))
99
100 info, err = os.Stat(filepath.Join(path, "hooks", "install"))
101 c.Assert(err, IsNil)
102 c.Assert(info.Mode()&0777, Equals, os.FileMode(0755))
103 c.Assert(info.Mode()&os.ModeType, Equals, os.FileMode(0))
104
105 info, err = os.Stat(filepath.Join(path, "empty"))
106 c.Assert(err, IsNil)
107 c.Assert(info.Mode()&0777, Equals, os.FileMode(0755))
108
109 target, err := os.Readlink(filepath.Join(path, "hooks", "symlink"))
110 c.Assert(err, IsNil)
111 c.Assert(target, Equals, "../target")
112}
113
114func (s *BundleSuite) TestBundleRevisionFile(c *C) {
115 charmDir := testing.Charms.ClonedDirPath(c.MkDir(), "dummy")
116 revPath := filepath.Join(charmDir, "revision")
117
118 // Missing revision file
119 err := os.Remove(revPath)
120 c.Assert(err, IsNil)
121
122 bundle, err := charm.ReadBundle(extBundleDir(c, charmDir))
123 c.Assert(err, IsNil)
124 c.Assert(bundle.Revision(), Equals, 0)
125
126 // Missing revision file with old revision in metadata
127 file, err := os.OpenFile(filepath.Join(charmDir, "metadata.yaml"), os.O_WRONLY|os.O_APPEND, 0)
128 c.Assert(err, IsNil)
129 _, err = file.Write([]byte("\nrevision: 1234\n"))
130 c.Assert(err, IsNil)
131
132 bundle, err = charm.ReadBundle(extBundleDir(c, charmDir))
133 c.Assert(err, IsNil)
134 c.Assert(bundle.Revision(), Equals, 1234)
135
136 // Revision file with bad content
137 err = ioutil.WriteFile(revPath, []byte("garbage"), 0666)
138 c.Assert(err, IsNil)
139
140 bundle, err = charm.ReadBundle(extBundleDir(c, charmDir))
141 c.Assert(err, ErrorMatches, "invalid revision file")
142 c.Assert(bundle, IsNil)
143}
144
145func (s *BundleSuite) TestBundleSetRevision(c *C) {
146 bundle, err := charm.ReadBundle(s.bundlePath)
147 c.Assert(err, IsNil)
148
149 c.Assert(bundle.Revision(), Equals, 1)
150 bundle.SetRevision(42)
151 c.Assert(bundle.Revision(), Equals, 42)
152
153 path := filepath.Join(c.MkDir(), "charm")
154 err = bundle.ExpandTo(path)
155 c.Assert(err, IsNil)
156
157 dir, err := charm.ReadDir(path)
158 c.Assert(err, IsNil)
159 c.Assert(dir.Revision(), Equals, 42)
160}
161
162func (s *BundleSuite) TestExpandToWithBadLink(c *C) {
163 charmDir := testing.Charms.ClonedDirPath(c.MkDir(), "dummy")
164 badLink := filepath.Join(charmDir, "hooks", "badlink")
165
166 // Symlink targeting a path outside of the charm.
167 err := os.Symlink("../../target", badLink)
168 c.Assert(err, IsNil)
169
170 bundle, err := charm.ReadBundle(extBundleDir(c, charmDir))
171 c.Assert(err, IsNil)
172
173 path := filepath.Join(c.MkDir(), "charm")
174 err = bundle.ExpandTo(path)
175 c.Assert(err, ErrorMatches, `symlink "hooks/badlink" links out of charm: "../../target"`)
176
177 // Symlink targeting an absolute path.
178 os.Remove(badLink)
179 err = os.Symlink("/target", badLink)
180 c.Assert(err, IsNil)
181
182 bundle, err = charm.ReadBundle(extBundleDir(c, charmDir))
183 c.Assert(err, IsNil)
184
185 path = filepath.Join(c.MkDir(), "charm")
186 err = bundle.ExpandTo(path)
187 c.Assert(err, ErrorMatches, `symlink "hooks/badlink" is absolute: "/target"`)
188}
189
190func extBundleDir(c *C, dirpath string) (path string) {
191 path = filepath.Join(c.MkDir(), "bundle.charm")
192 cmd := exec.Command("/bin/sh", "-c", fmt.Sprintf("cd %s; zip --fifo --symlinks -r %s .", dirpath, path))
193 output, err := cmd.CombinedOutput()
194 c.Assert(err, IsNil, Commentf("Command output: %s", output))
195 return path
196}
0197
=== added file 'charm/charm_test.go.OTHER'
--- charm/charm_test.go.OTHER 1970-01-01 00:00:00 +0000
+++ charm/charm_test.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,97 @@
1package charm_test
2
3import (
4 "bytes"
5 "io"
6 "io/ioutil"
7 . "launchpad.net/gocheck"
8 "launchpad.net/goyaml"
9 "launchpad.net/juju-core/juju/charm"
10 "launchpad.net/juju-core/juju/testing"
11 stdtesting "testing"
12)
13
14func Test(t *stdtesting.T) {
15 TestingT(t)
16}
17
18type CharmSuite struct{}
19
20var _ = Suite(&CharmSuite{})
21
22func (s *CharmSuite) TestRead(c *C) {
23 bPath := testing.Charms.BundlePath(c.MkDir(), "dummy")
24 ch, err := charm.Read(bPath)
25 c.Assert(err, IsNil)
26 c.Assert(ch.Meta().Name, Equals, "dummy")
27 dPath := testing.Charms.DirPath("dummy")
28 ch, err = charm.Read(dPath)
29 c.Assert(err, IsNil)
30 c.Assert(ch.Meta().Name, Equals, "dummy")
31}
32
33var inferRepoTests = []struct {
34 name string
35 series string
36 path string
37 curl string
38}{
39 {"wordpress", "precise", "anything", "cs:precise/wordpress"},
40 {"oneiric/wordpress", "anything", "anything", "cs:oneiric/wordpress"},
41 {"cs:oneiric/wordpress", "anything", "anything", "cs:oneiric/wordpress"},
42 {"local:wordpress", "precise", "/some/path", "local:precise/wordpress"},
43 {"local:oneiric/wordpress", "anything", "/some/path", "local:oneiric/wordpress"},
44}
45
46func (s *CharmSuite) TestInferRepository(c *C) {
47 for _, t := range inferRepoTests {
48 repo, curl, err := charm.InferRepository(t.name, t.series, t.path)
49 c.Assert(err, IsNil)
50 expectCurl := charm.MustParseURL(t.curl)
51 c.Assert(curl, DeepEquals, expectCurl)
52 if localRepo, ok := repo.(*charm.LocalRepository); ok {
53 c.Assert(localRepo.Path, Equals, t.path)
54 c.Assert(curl.Schema, Equals, "local")
55 } else {
56 c.Assert(curl.Schema, Equals, "cs")
57 }
58 }
59 _, _, err := charm.InferRepository("local:whatever", "series", "")
60 c.Assert(err, ErrorMatches, "path to local repository not specified")
61}
62
63func checkDummy(c *C, f charm.Charm, path string) {
64 c.Assert(f.Revision(), Equals, 1)
65 c.Assert(f.Meta().Name, Equals, "dummy")
66 c.Assert(f.Config().Options["title"].Default, Equals, "My Title")
67 switch f := f.(type) {
68 case *charm.Bundle:
69 c.Assert(f.Path, Equals, path)
70
71 case *charm.Dir:
72 c.Assert(f.Path, Equals, path)
73 }
74}
75
76type YamlHacker map[interface{}]interface{}
77
78func ReadYaml(r io.Reader) YamlHacker {
79 data, err := ioutil.ReadAll(r)
80 if err != nil {
81 panic(err)
82 }
83 m := make(map[interface{}]interface{})
84 err = goyaml.Unmarshal(data, m)
85 if err != nil {
86 panic(err)
87 }
88 return YamlHacker(m)
89}
90
91func (yh YamlHacker) Reader() io.Reader {
92 data, err := goyaml.Marshal(yh)
93 if err != nil {
94 panic(err)
95 }
96 return bytes.NewBuffer(data)
97}
098
=== added file 'charm/config.go.OTHER'
--- charm/config.go.OTHER 1970-01-01 00:00:00 +0000
+++ charm/config.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,141 @@
1package charm
2
3import (
4 "errors"
5 "fmt"
6 "io"
7 "io/ioutil"
8 "launchpad.net/goyaml"
9 "launchpad.net/juju-core/juju/schema"
10 "reflect"
11 "strconv"
12)
13
14// Option represents a single configuration option that is declared
15// as supported by a charm in its config.yaml file.
16type Option struct {
17 Title string
18 Description string
19 Type string
20 Default interface{}
21}
22
23// Config represents the supported configuration options for a charm,
24// as declared in its config.yaml file.
25type Config struct {
26 Options map[string]Option
27}
28
29// NewConfig returns a new Config without any options.
30func NewConfig() *Config {
31 return &Config{make(map[string]Option)}
32}
33
34// ReadConfig reads a config.yaml file and returns its representation.
35func ReadConfig(r io.Reader) (config *Config, err error) {
36 data, err := ioutil.ReadAll(r)
37 if err != nil {
38 return
39 }
40 raw := make(map[interface{}]interface{})
41 err = goyaml.Unmarshal(data, raw)
42 if err != nil {
43 return
44 }
45 v, err := configSchema.Coerce(raw, nil)
46 if err != nil {
47 return nil, errors.New("config: " + err.Error())
48 }
49 config = NewConfig()
50 m := v.(schema.MapType)
51 for name, infov := range m["options"].(schema.MapType) {
52 opt := infov.(schema.MapType)
53 optTitle, _ := opt["title"].(string)
54 optType, _ := opt["type"].(string)
55 optDescr, _ := opt["description"].(string)
56 optDefault := opt["default"]
57 if optDefault != nil {
58 if reflect.TypeOf(optDefault).Kind() != validTypes[optType] {
59 msg := "Bad default for %q: %v is not of type %s"
60 return nil, fmt.Errorf(msg, name, optDefault, optType)
61 }
62 }
63 config.Options[name.(string)] = Option{
64 Title: optTitle,
65 Type: optType,
66 Description: optDescr,
67 Default: optDefault,
68 }
69 }
70 return
71}
72
73// Validate processes the values in the input map according to the
74// configuration in config, doing the following operations:
75//
76// - Values are converted from strings to the types defined
77// - Options with default values are introduced for missing keys
78// - Unknown keys and badly typed values are reported as errors
79//
80func (c *Config) Validate(values map[string]string) (processed map[string]interface{}, err error) {
81 out := make(map[string]interface{})
82 for k, v := range values {
83 opt, ok := c.Options[k]
84 if !ok {
85 return nil, fmt.Errorf("Unknown configuration option: %q", k)
86 }
87 switch opt.Type {
88 case "string":
89 out[k] = v
90 case "int":
91 i, err := strconv.ParseInt(v, 10, 64)
92 if err != nil {
93 return nil, fmt.Errorf("Value for %q is not an int: %q", k, v)
94 }
95 out[k] = i
96 case "float":
97 f, err := strconv.ParseFloat(v, 64)
98 if err != nil {
99 return nil, fmt.Errorf("Value for %q is not a float: %q", k, v)
100 }
101 out[k] = f
102 case "boolean":
103 b, err := strconv.ParseBool(v)
104 if err != nil {
105 return nil, fmt.Errorf("Value for %q is not a boolean: %q", k, v)
106 }
107 out[k] = b
108 default:
109 panic(fmt.Errorf("Internal error: option type %q is unknown to Validate", opt.Type))
110 }
111 }
112 for k, opt := range c.Options {
113 if _, ok := out[k]; !ok && opt.Default != nil {
114 out[k] = opt.Default
115 }
116 }
117 return out, nil
118}
119
120var validTypes = map[string]reflect.Kind{
121 "string": reflect.String,
122 "int": reflect.Int64,
123 "boolean": reflect.Bool,
124 "float": reflect.Float64,
125}
126
127var optionSchema = schema.FieldMap(
128 schema.Fields{
129 "type": schema.OneOf(schema.Const("string"), schema.Const("int"), schema.Const("float"), schema.Const("boolean")),
130 "default": schema.OneOf(schema.String(), schema.Int(), schema.Float(), schema.Bool()),
131 "description": schema.String(),
132 },
133 schema.Optional{"default", "description"},
134)
135
136var configSchema = schema.FieldMap(
137 schema.Fields{
138 "options": schema.Map(schema.String(), optionSchema),
139 },
140 nil,
141)
0142
=== added file 'charm/config_test.go.OTHER'
--- charm/config_test.go.OTHER 1970-01-01 00:00:00 +0000
+++ charm/config_test.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,191 @@
1package charm_test
2
3import (
4 "bytes"
5 "fmt"
6 "io"
7 "io/ioutil"
8 . "launchpad.net/gocheck"
9 "launchpad.net/juju-core/juju/charm"
10 "launchpad.net/juju-core/juju/testing"
11 "os"
12 "path/filepath"
13)
14
15var sampleConfig = `
16options:
17 title:
18 default: My Title
19 description: A descriptive title used for the service.
20 type: string
21 outlook:
22 description: No default outlook.
23 type: string
24 username:
25 default: admin001
26 description: The name of the initial account (given admin permissions).
27 type: string
28 skill-level:
29 description: A number indicating skill.
30 type: int
31 agility-ratio:
32 description: A number from 0 to 1 indicating agility.
33 type: float
34 reticulate-splines:
35 description: Whether to reticulate splines on launch, or not.
36 type: boolean
37`
38
39func repoConfig(name string) io.Reader {
40 charmDir := testing.Charms.DirPath(name)
41 file, err := os.Open(filepath.Join(charmDir, "config.yaml"))
42 if err != nil {
43 panic(err)
44 }
45 defer file.Close()
46 data, err := ioutil.ReadAll(file)
47 if err != nil {
48 panic(err)
49 }
50 return bytes.NewBuffer(data)
51}
52
53type ConfigSuite struct{}
54
55var _ = Suite(&ConfigSuite{})
56
57func (s *ConfigSuite) TestReadConfig(c *C) {
58 config, err := charm.ReadConfig(repoConfig("dummy"))
59 c.Assert(err, IsNil)
60 c.Assert(config.Options["title"], DeepEquals,
61 charm.Option{
62 Default: "My Title",
63 Description: "A descriptive title used for the service.",
64 Type: "string",
65 },
66 )
67}
68
69func (s *ConfigSuite) TestConfigError(c *C) {
70 _, err := charm.ReadConfig(bytes.NewBuffer([]byte(`options: {t: {type: foo}}`)))
71 c.Assert(err, ErrorMatches, `config: options.t.type: unsupported value`)
72}
73
74func (s *ConfigSuite) TestDefaultType(c *C) {
75 assertDefault := func(type_ string, value string, expected interface{}) {
76 config := fmt.Sprintf(`options: {t: {type: %s, default: %s}}`, type_, value)
77 result, err := charm.ReadConfig(bytes.NewBuffer([]byte(config)))
78 c.Assert(err, IsNil)
79 c.Assert(result.Options["t"].Default, Equals, expected)
80 }
81
82 assertDefault("boolean", "true", true)
83 assertDefault("string", "golden grahams", "golden grahams")
84 assertDefault("float", "2.2e11", 2.2e11)
85 assertDefault("int", "99", int64(99))
86
87 assertTypeError := func(type_ string, value string) {
88 config := fmt.Sprintf(`options: {t: {type: %s, default: %s}}`, type_, value)
89 _, err := charm.ReadConfig(bytes.NewBuffer([]byte(config)))
90 expected := fmt.Sprintf(`Bad default for "t": %s is not of type %s`, value, type_)
91 c.Assert(err, ErrorMatches, expected)
92 }
93
94 assertTypeError("boolean", "henry")
95 assertTypeError("string", "2.5")
96 assertTypeError("float", "blob")
97 assertTypeError("int", "33.2")
98}
99
100func (s *ConfigSuite) TestParseSample(c *C) {
101 config, err := charm.ReadConfig(bytes.NewBuffer([]byte(sampleConfig)))
102 c.Assert(err, IsNil)
103
104 opt := config.Options
105 c.Assert(opt["title"], DeepEquals,
106 charm.Option{
107 Default: "My Title",
108 Description: "A descriptive title used for the service.",
109 Type: "string",
110 },
111 )
112 c.Assert(opt["outlook"], DeepEquals,
113 charm.Option{
114 Description: "No default outlook.",
115 Type: "string",
116 },
117 )
118 c.Assert(opt["username"], DeepEquals,
119 charm.Option{
120 Default: "admin001",
121 Description: "The name of the initial account (given admin permissions).",
122 Type: "string",
123 },
124 )
125 c.Assert(opt["skill-level"], DeepEquals,
126 charm.Option{
127 Description: "A number indicating skill.",
128 Type: "int",
129 },
130 )
131 c.Assert(opt["reticulate-splines"], DeepEquals,
132 charm.Option{
133 Description: "Whether to reticulate splines on launch, or not.",
134 Type: "boolean",
135 },
136 )
137}
138
139func (s *ConfigSuite) TestValidate(c *C) {
140 config, err := charm.ReadConfig(bytes.NewBuffer([]byte(sampleConfig)))
141 c.Assert(err, IsNil)
142
143 input := map[string]string{
144 "title": "Helpful Title",
145 "outlook": "Peachy",
146 }
147
148 // This should include an overridden value, a default and a new value.
149 expected := map[string]interface{}{
150 "title": "Helpful Title",
151 "outlook": "Peachy",
152 "username": "admin001",
153 }
154
155 output, err := config.Validate(input)
156 c.Assert(err, IsNil)
157 c.Assert(output, DeepEquals, expected)
158
159 // Check whether float conversion is working.
160 input["agility-ratio"] = "0.5"
161 input["skill-level"] = "7"
162 expected["agility-ratio"] = 0.5
163 expected["skill-level"] = int64(7)
164 output, err = config.Validate(input)
165 c.Assert(err, IsNil)
166 c.Assert(output, DeepEquals, expected)
167
168 // Check whether float errors are caught.
169 input["agility-ratio"] = "foo"
170 output, err = config.Validate(input)
171 c.Assert(err, ErrorMatches, `Value for "agility-ratio" is not a float: "foo"`)
172 input["agility-ratio"] = "0.5"
173
174 // Check whether int errors are caught.
175 input["skill-level"] = "foo"
176 output, err = config.Validate(input)
177 c.Assert(err, ErrorMatches, `Value for "skill-level" is not an int: "foo"`)
178 input["skill-level"] = "7"
179
180 // Check whether boolean errors are caught.
181 input["reticulate-splines"] = "maybe"
182 output, err = config.Validate(input)
183 c.Assert(err, ErrorMatches, `Value for "reticulate-splines" is not a boolean: "maybe"`)
184 input["reticulate-splines"] = "false"
185
186 // Now try to set a value outside the expected.
187 input["bad"] = "value"
188 output, err = config.Validate(input)
189 c.Assert(output, IsNil)
190 c.Assert(err, ErrorMatches, `Unknown configuration option: "bad"`)
191}
0192
=== added file 'charm/dir_test.go.OTHER'
--- charm/dir_test.go.OTHER 1970-01-01 00:00:00 +0000
+++ charm/dir_test.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,199 @@
1package charm_test
2
3import (
4 "archive/zip"
5 "bytes"
6 "io/ioutil"
7 . "launchpad.net/gocheck"
8 "launchpad.net/juju-core/juju/charm"
9 "launchpad.net/juju-core/juju/testing"
10 "os"
11 "path/filepath"
12 "syscall"
13)
14
15type DirSuite struct{}
16
17var _ = Suite(&DirSuite{})
18
19func (s *DirSuite) TestReadDir(c *C) {
20 path := testing.Charms.DirPath("dummy")
21 dir, err := charm.ReadDir(path)
22 c.Assert(err, IsNil)
23 checkDummy(c, dir, path)
24}
25
26func (s *DirSuite) TestReadDirWithoutConfig(c *C) {
27 path := testing.Charms.DirPath("varnish")
28 dir, err := charm.ReadDir(path)
29 c.Assert(err, IsNil)
30
31 // A lacking config.yaml file still causes a proper
32 // Config value to be returned.
33 c.Assert(dir.Config().Options, HasLen, 0)
34}
35
36func (s *DirSuite) TestBundleTo(c *C) {
37 dir := testing.Charms.Dir("dummy")
38 path := filepath.Join(c.MkDir(), "bundle.charm")
39 file, err := os.Create(path)
40 c.Assert(err, IsNil)
41 err = dir.BundleTo(file)
42 file.Close()
43 c.Assert(err, IsNil)
44
45 zipr, err := zip.OpenReader(path)
46 c.Assert(err, IsNil)
47 defer zipr.Close()
48
49 var metaf, instf, emptyf, revf, symf *zip.File
50 for _, f := range zipr.File {
51 c.Logf("Bundled file: %s", f.Name)
52 switch f.Name {
53 case "revision":
54 revf = f
55 case "metadata.yaml":
56 metaf = f
57 case "hooks/install":
58 instf = f
59 case "hooks/symlink":
60 symf = f
61 case "empty/":
62 emptyf = f
63 case "build/ignored":
64 c.Errorf("bundle includes build/*: %s", f.Name)
65 case ".ignored", ".dir/ignored":
66 c.Errorf("bundle includes .* entries: %s", f.Name)
67 }
68 }
69
70 c.Assert(revf, NotNil)
71 reader, err := revf.Open()
72 c.Assert(err, IsNil)
73 data, err := ioutil.ReadAll(reader)
74 reader.Close()
75 c.Assert(err, IsNil)
76 c.Assert(string(data), Equals, "1")
77
78 c.Assert(metaf, NotNil)
79 reader, err = metaf.Open()
80 c.Assert(err, IsNil)
81 meta, err := charm.ReadMeta(reader)
82 reader.Close()
83 c.Assert(err, IsNil)
84 c.Assert(meta.Name, Equals, "dummy")
85
86 c.Assert(instf, NotNil)
87 // Despite it being 0751, we pack and unpack it as 0755.
88 c.Assert(instf.Mode()&0777, Equals, os.FileMode(0755))
89
90 c.Assert(symf, NotNil)
91 c.Assert(symf.Mode()&0777, Equals, os.FileMode(0777))
92 reader, err = symf.Open()
93 c.Assert(err, IsNil)
94 data, err = ioutil.ReadAll(reader)
95 reader.Close()
96 c.Assert(err, IsNil)
97 c.Assert(string(data), Equals, "../target")
98
99 c.Assert(emptyf, NotNil)
100 c.Assert(emptyf.Mode()&os.ModeType, Equals, os.ModeDir)
101 // Despite it being 0750, we pack and unpack it as 0755.
102 c.Assert(emptyf.Mode()&0777, Equals, os.FileMode(0755))
103}
104
105func (s *DirSuite) TestBundleToWithBadType(c *C) {
106 charmDir := testing.Charms.ClonedDirPath(c.MkDir(), "dummy")
107 badFile := filepath.Join(charmDir, "hooks", "badfile")
108
109 // Symlink targeting a path outside of the charm.
110 err := os.Symlink("../../target", badFile)
111 c.Assert(err, IsNil)
112
113 dir, err := charm.ReadDir(charmDir)
114 c.Assert(err, IsNil)
115
116 err = dir.BundleTo(&bytes.Buffer{})
117 c.Assert(err, ErrorMatches, `symlink "hooks/badfile" links out of charm: "../../target"`)
118
119 // Symlink targeting an absolute path.
120 os.Remove(badFile)
121 err = os.Symlink("/target", badFile)
122 c.Assert(err, IsNil)
123
124 dir, err = charm.ReadDir(charmDir)
125 c.Assert(err, IsNil)
126
127 err = dir.BundleTo(&bytes.Buffer{})
128 c.Assert(err, ErrorMatches, `symlink "hooks/badfile" is absolute: "/target"`)
129
130 // Can't bundle special files either.
131 os.Remove(badFile)
132 err = syscall.Mkfifo(badFile, 0644)
133 c.Assert(err, IsNil)
134
135 dir, err = charm.ReadDir(charmDir)
136 c.Assert(err, IsNil)
137
138 err = dir.BundleTo(&bytes.Buffer{})
139 c.Assert(err, ErrorMatches, `file is a named pipe: "hooks/badfile"`)
140}
141
142func (s *DirSuite) TestDirRevisionFile(c *C) {
143 charmDir := testing.Charms.ClonedDirPath(c.MkDir(), "dummy")
144 revPath := filepath.Join(charmDir, "revision")
145
146 // Missing revision file
147 err := os.Remove(revPath)
148 c.Assert(err, IsNil)
149
150 dir, err := charm.ReadDir(charmDir)
151 c.Assert(err, IsNil)
152 c.Assert(dir.Revision(), Equals, 0)
153
154 // Missing revision file with old revision in metadata
155 file, err := os.OpenFile(filepath.Join(charmDir, "metadata.yaml"), os.O_WRONLY|os.O_APPEND, 0)
156 c.Assert(err, IsNil)
157 _, err = file.Write([]byte("\nrevision: 1234\n"))
158 c.Assert(err, IsNil)
159
160 dir, err = charm.ReadDir(charmDir)
161 c.Assert(err, IsNil)
162 c.Assert(dir.Revision(), Equals, 1234)
163
164 // Revision file with bad content
165 err = ioutil.WriteFile(revPath, []byte("garbage"), 0666)
166 c.Assert(err, IsNil)
167
168 dir, err = charm.ReadDir(charmDir)
169 c.Assert(err, ErrorMatches, "invalid revision file")
170 c.Assert(dir, IsNil)
171}
172
173func (s *DirSuite) TestDirSetRevision(c *C) {
174 dir := testing.Charms.ClonedDir(c.MkDir(), "dummy")
175 c.Assert(dir.Revision(), Equals, 1)
176 dir.SetRevision(42)
177 c.Assert(dir.Revision(), Equals, 42)
178
179 var b bytes.Buffer
180 err := dir.BundleTo(&b)
181 c.Assert(err, IsNil)
182
183 bundle, err := charm.ReadBundleBytes(b.Bytes())
184 c.Assert(bundle.Revision(), Equals, 42)
185}
186
187func (s *DirSuite) TestDirSetDiskRevision(c *C) {
188 charmDir := testing.Charms.ClonedDirPath(c.MkDir(), "dummy")
189 dir, err := charm.ReadDir(charmDir)
190 c.Assert(err, IsNil)
191
192 c.Assert(dir.Revision(), Equals, 1)
193 dir.SetDiskRevision(42)
194 c.Assert(dir.Revision(), Equals, 42)
195
196 dir, err = charm.ReadDir(charmDir)
197 c.Assert(err, IsNil)
198 c.Assert(dir.Revision(), Equals, 42)
199}
0200
=== added file 'charm/export_test.go.OTHER'
--- charm/export_test.go.OTHER 1970-01-01 00:00:00 +0000
+++ charm/export_test.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,15 @@
1package charm
2
3import (
4 "launchpad.net/juju-core/juju/schema"
5)
6
7// Export meaningful bits for tests only.
8
9func IfaceExpander(limit interface{}) schema.Checker {
10 return ifaceExpander(limit)
11}
12
13func NewStore(url, path string) Repository {
14 return &store{url, path}
15}
016
=== added file 'charm/meta.go.OTHER'
--- charm/meta.go.OTHER 1970-01-01 00:00:00 +0000
+++ charm/meta.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,201 @@
1package charm
2
3import (
4 "errors"
5 "fmt"
6 "io"
7 "io/ioutil"
8 "launchpad.net/goyaml"
9 "launchpad.net/juju-core/juju/schema"
10)
11
12const (
13 ScopeGlobal = "global"
14 ScopeContainer = "container"
15)
16
17// Relation represents a single relation defined in the charm
18// metadata.yaml file.
19type Relation struct {
20 Interface string
21 Optional bool
22 Limit int
23 Scope string
24}
25
26// Meta represents all the known content that may be defined
27// within a charm's metadata.yaml file.
28type Meta struct {
29 Name string
30 Summary string
31 Description string
32 Provides map[string]Relation
33 Requires map[string]Relation
34 Peers map[string]Relation
35 OldRevision int // Obsolete
36 Subordinate bool
37}
38
39// ReadMeta reads the content of a metadata.yaml file and returns
40// its representation.
41func ReadMeta(r io.Reader) (meta *Meta, err error) {
42 data, err := ioutil.ReadAll(r)
43 if err != nil {
44 return
45 }
46 raw := make(map[interface{}]interface{})
47 err = goyaml.Unmarshal(data, raw)
48 if err != nil {
49 return
50 }
51 v, err := charmSchema.Coerce(raw, nil)
52 if err != nil {
53 return nil, errors.New("metadata: " + err.Error())
54 }
55 m := v.(schema.MapType)
56 meta = &Meta{}
57 meta.Name = m["name"].(string)
58 // Schema decodes as int64, but the int range should be good
59 // enough for revisions.
60 meta.Summary = m["summary"].(string)
61 meta.Description = m["description"].(string)
62 meta.Provides = parseRelations(m["provides"])
63 meta.Requires = parseRelations(m["requires"])
64 meta.Peers = parseRelations(m["peers"])
65 if rev := m["revision"]; rev != nil {
66 // Obsolete
67 meta.OldRevision = int(m["revision"].(int64))
68 }
69 // Subordinate charms must have at least one relation that
70 // has container scope, otherwise they can't relate to the
71 // principal.
72 if subordinate := m["subordinate"]; subordinate != nil {
73 valid := false
74 if meta.Requires != nil {
75 for _, relationData := range meta.Requires {
76 if relationData.Scope == ScopeContainer {
77 valid = true
78 break
79 }
80 }
81 }
82 if !valid {
83 return nil, fmt.Errorf("subordinate charm %q lacks requires relation with container scope", meta.Name)
84 }
85 meta.Subordinate = m["subordinate"].(bool)
86 }
87 return
88}
89
90func parseRelations(relations interface{}) map[string]Relation {
91 if relations == nil {
92 return nil
93 }
94 result := make(map[string]Relation)
95 for name, rel := range relations.(schema.MapType) {
96 relMap := rel.(schema.MapType)
97 relation := Relation{}
98 relation.Interface = relMap["interface"].(string)
99 relation.Optional = relMap["optional"].(bool)
100 if scope := relMap["scope"]; scope != nil {
101 relation.Scope = scope.(string)
102 }
103 if relMap["limit"] != nil {
104 // Schema defaults to int64, but we know
105 // the int range should be more than enough.
106 relation.Limit = int(relMap["limit"].(int64))
107 }
108 result[name.(string)] = relation
109 }
110 return result
111}
112
113// Schema coercer that expands the interface shorthand notation.
114// A consistent format is easier to work with than considering the
115// potential difference everywhere.
116//
117// Supports the following variants::
118//
119// provides:
120// server: riak
121// admin: http
122// foobar:
123// interface: blah
124//
125// provides:
126// server:
127// interface: mysql
128// limit:
129// optional: false
130//
131// In all input cases, the output is the fully specified interface
132// representation as seen in the mysql interface description above.
133func ifaceExpander(limit interface{}) schema.Checker {
134 return ifaceExpC{limit}
135}
136
137type ifaceExpC struct {
138 limit interface{}
139}
140
141var (
142 stringC = schema.String()
143 mapC = schema.Map(schema.String(), schema.Any())
144)
145
146func (c ifaceExpC) Coerce(v interface{}, path []string) (newv interface{}, err error) {
147 s, err := stringC.Coerce(v, path)
148 if err == nil {
149 newv = schema.MapType{
150 "interface": s,
151 "limit": c.limit,
152 "optional": false,
153 "scope": ScopeGlobal,
154 }
155 return
156 }
157
158 // Optional values are context-sensitive and/or have
159 // defaults, which is different than what KeyDict can
160 // readily support. So just do it here first, then
161 // coerce to the real schema.
162 v, err = mapC.Coerce(v, path)
163 if err != nil {
164 return
165 }
166 m := v.(schema.MapType)
167 if _, ok := m["limit"]; !ok {
168 m["limit"] = c.limit
169 }
170 if _, ok := m["optional"]; !ok {
171 m["optional"] = false
172 }
173 if _, ok := m["scope"]; !ok {
174 m["scope"] = ScopeGlobal
175 }
176 return ifaceSchema.Coerce(m, path)
177}
178
179var ifaceSchema = schema.FieldMap(
180 schema.Fields{
181 "interface": schema.String(),
182 "limit": schema.OneOf(schema.Const(nil), schema.Int()),
183 "scope": schema.OneOf(schema.Const(ScopeGlobal), schema.Const(ScopeContainer)),
184 "optional": schema.Bool(),
185 },
186 schema.Optional{"scope"},
187)
188
189var charmSchema = schema.FieldMap(
190 schema.Fields{
191 "name": schema.String(),
192 "summary": schema.String(),
193 "description": schema.String(),
194 "peers": schema.Map(schema.String(), ifaceExpander(1)),
195 "provides": schema.Map(schema.String(), ifaceExpander(nil)),
196 "requires": schema.Map(schema.String(), ifaceExpander(1)),
197 "revision": schema.Int(), // Obsolete
198 "subordinate": schema.Bool(),
199 },
200 schema.Optional{"provides", "requires", "peers", "revision", "subordinate"},
201)
0202
=== added file 'charm/meta_test.go.OTHER'
--- charm/meta_test.go.OTHER 1970-01-01 00:00:00 +0000
+++ charm/meta_test.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,137 @@
1package charm_test
2
3import (
4 "bytes"
5 "io"
6 "io/ioutil"
7 . "launchpad.net/gocheck"
8 "launchpad.net/juju-core/juju/charm"
9 "launchpad.net/juju-core/juju/schema"
10 "launchpad.net/juju-core/juju/testing"
11 "os"
12 "path/filepath"
13)
14
15func repoMeta(name string) io.Reader {
16 charmDir := testing.Charms.DirPath(name)
17 file, err := os.Open(filepath.Join(charmDir, "metadata.yaml"))
18 if err != nil {
19 panic(err)
20 }
21 defer file.Close()
22 data, err := ioutil.ReadAll(file)
23 if err != nil {
24 panic(err)
25 }
26 return bytes.NewBuffer(data)
27}
28
29type MetaSuite struct{}
30
31var _ = Suite(&MetaSuite{})
32
33func (s *MetaSuite) TestReadMeta(c *C) {
34 meta, err := charm.ReadMeta(repoMeta("dummy"))
35 c.Assert(err, IsNil)
36 c.Assert(meta.Name, Equals, "dummy")
37 c.Assert(meta.Summary, Equals, "That's a dummy charm.")
38 c.Assert(meta.Description, Equals,
39 "This is a longer description which\npotentially contains multiple lines.\n")
40 c.Assert(meta.OldRevision, Equals, 0)
41 c.Assert(meta.Subordinate, Equals, false)
42}
43
44func (s *MetaSuite) TestSubordinate(c *C) {
45 meta, err := charm.ReadMeta(repoMeta("logging"))
46 c.Assert(err, IsNil)
47 c.Assert(meta.Subordinate, Equals, true)
48}
49
50func (s *MetaSuite) TestSubordinateWithoutContainerRelation(c *C) {
51 r := repoMeta("dummy")
52 hackYaml := ReadYaml(r)
53 hackYaml["subordinate"] = true
54 _, err := charm.ReadMeta(hackYaml.Reader())
55 c.Assert(err, ErrorMatches, "subordinate charm \"dummy\" lacks requires relation with container scope")
56}
57
58func (s *MetaSuite) TestScopeConstraint(c *C) {
59 meta, err := charm.ReadMeta(repoMeta("logging"))
60 c.Assert(err, IsNil)
61 c.Assert(meta.Provides["logging-client"].Scope, Equals, charm.ScopeGlobal)
62 c.Assert(meta.Requires["logging-directory"].Scope, Equals, charm.ScopeContainer)
63 c.Assert(meta.Subordinate, Equals, true)
64}
65
66func (s *MetaSuite) TestParseMetaRelations(c *C) {
67 meta, err := charm.ReadMeta(repoMeta("mysql"))
68 c.Assert(err, IsNil)
69 c.Assert(meta.Provides["server"], Equals, charm.Relation{Interface: "mysql", Scope: charm.ScopeGlobal})
70 c.Assert(meta.Requires, IsNil)
71 c.Assert(meta.Peers, IsNil)
72
73 meta, err = charm.ReadMeta(repoMeta("riak"))
74 c.Assert(err, IsNil)
75 c.Assert(meta.Provides["endpoint"], Equals, charm.Relation{Interface: "http", Scope: charm.ScopeGlobal})
76 c.Assert(meta.Provides["admin"], Equals, charm.Relation{Interface: "http", Scope: charm.ScopeGlobal})
77 c.Assert(meta.Peers["ring"], Equals, charm.Relation{Interface: "riak", Limit: 1, Scope: charm.ScopeGlobal})
78 c.Assert(meta.Requires, IsNil)
79
80 meta, err = charm.ReadMeta(repoMeta("wordpress"))
81 c.Assert(err, IsNil)
82 c.Assert(meta.Provides["url"], Equals, charm.Relation{Interface: "http", Scope: charm.ScopeGlobal})
83 c.Assert(meta.Requires["db"], Equals, charm.Relation{Interface: "mysql", Limit: 1, Scope: charm.ScopeGlobal})
84 c.Assert(meta.Requires["cache"], Equals, charm.Relation{Interface: "varnish", Limit: 2, Optional: true, Scope: charm.ScopeGlobal})
85 c.Assert(meta.Peers, IsNil)
86
87}
88
89// Test rewriting of a given interface specification into long form.
90//
91// InterfaceExpander uses `coerce` to do one of two things:
92//
93// - Rewrite shorthand to the long form used for actual storage
94// - Fills in defaults, including a configurable `limit`
95//
96// This test ensures test coverage on each of these branches, along
97// with ensuring the conversion object properly raises SchemaError
98// exceptions on invalid data.
99func (s *MetaSuite) TestIfaceExpander(c *C) {
100 e := charm.IfaceExpander(nil)
101
102 path := []string{"<pa", "th>"}
103
104 // Shorthand is properly rewritten
105 v, err := e.Coerce("http", path)
106 c.Assert(err, IsNil)
107 c.Assert(v, DeepEquals, schema.MapType{"interface": "http", "limit": nil, "optional": false, "scope": charm.ScopeGlobal})
108
109 // Defaults are properly applied
110 v, err = e.Coerce(schema.MapType{"interface": "http"}, path)
111 c.Assert(err, IsNil)
112 c.Assert(v, DeepEquals, schema.MapType{"interface": "http", "limit": nil, "optional": false, "scope": charm.ScopeGlobal})
113
114 v, err = e.Coerce(schema.MapType{"interface": "http", "limit": 2}, path)
115 c.Assert(err, IsNil)
116 c.Assert(v, DeepEquals, schema.MapType{"interface": "http", "limit": int64(2), "optional": false, "scope": charm.ScopeGlobal})
117
118 v, err = e.Coerce(schema.MapType{"interface": "http", "optional": true}, path)
119 c.Assert(err, IsNil)
120 c.Assert(v, DeepEquals, schema.MapType{"interface": "http", "limit": nil, "optional": true, "scope": charm.ScopeGlobal})
121
122 // Invalid data raises an error.
123 v, err = e.Coerce(42, path)
124 c.Assert(err, ErrorMatches, "<path>: expected map, got 42")
125
126 v, err = e.Coerce(schema.MapType{"interface": "http", "optional": nil}, path)
127 c.Assert(err, ErrorMatches, "<path>.optional: expected bool, got nothing")
128
129 v, err = e.Coerce(schema.MapType{"interface": "http", "limit": "none, really"}, path)
130 c.Assert(err, ErrorMatches, "<path>.limit: unsupported value")
131
132 // Can change default limit
133 e = charm.IfaceExpander(1)
134 v, err = e.Coerce(schema.MapType{"interface": "http"}, path)
135 c.Assert(err, IsNil)
136 c.Assert(v, DeepEquals, schema.MapType{"interface": "http", "limit": int64(1), "optional": false, "scope": charm.ScopeGlobal})
137}
0138
=== added file 'charm/repo.go.OTHER'
--- charm/repo.go.OTHER 1970-01-01 00:00:00 +0000
+++ charm/repo.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,226 @@
1package charm
2
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "encoding/json"
7 "fmt"
8 "io"
9 "io/ioutil"
10 "launchpad.net/juju-core/juju/log"
11 "net/http"
12 "net/url"
13 "os"
14 "path/filepath"
15 "strings"
16)
17
18// InfoResponse is sent by the charm store in response to charm-info requests.
19type InfoResponse struct {
20 Revision int `json:"revision"` // Zero is valid. Can't omitempty.
21 Sha256 string `json:"sha256,omitempty"`
22 Errors []string `json:"errors,omitempty"`
23 Warnings []string `json:"warnings,omitempty"`
24}
25
26// Repository respresents a collection of charms.
27type Repository interface {
28 Get(curl *URL) (Charm, error)
29 Latest(curl *URL) (int, error)
30}
31
32// store is a Repository that talks to the juju charm server (in ../store).
33type store struct {
34 baseURL string
35 cachePath string
36}
37
38const (
39 storeURL = "https://store.juju.ubuntu.com"
40 cachePath = "$HOME/.juju/cache"
41)
42
43// Store returns a Repository that provides access to the juju charm store.
44func Store() Repository {
45 return &store{storeURL, os.ExpandEnv(cachePath)}
46}
47
48// info returns the revision and SHA256 digest of the charm referenced by curl.
49func (s *store) info(curl *URL) (rev int, digest string, err error) {
50 key := curl.String()
51 resp, err := http.Get(s.baseURL + "/charm-info?charms=" + url.QueryEscape(key))
52 if err != nil {
53 return
54 }
55 defer resp.Body.Close()
56 body, err := ioutil.ReadAll(resp.Body)
57 if err != nil {
58 return
59 }
60 infos := make(map[string]*InfoResponse)
61 if err = json.Unmarshal(body, &infos); err != nil {
62 return
63 }
64 info, found := infos[key]
65 if !found {
66 err = fmt.Errorf("charm: charm store returned response without charm %q", key)
67 return
68 }
69 for _, w := range info.Warnings {
70 log.Printf("WARNING: charm store reports for %q: %s", key, w)
71 }
72 if info.Errors != nil {
73 err = fmt.Errorf(
74 "charm info errors for %q: %s", key, strings.Join(info.Errors, "; "),
75 )
76 return
77 }
78 return info.Revision, info.Sha256, nil
79}
80
81// Latest returns the latest revision of the charm referenced by curl, regardless
82// of the revision set on curl itself.
83func (s *store) Latest(curl *URL) (int, error) {
84 rev, _, err := s.info(curl.WithRevision(-1))
85 return rev, err
86}
87
88// verify returns an error unless a file exists at path with a hex-encoded
89// SHA256 matching digest.
90func verify(path, digest string) error {
91 b, err := ioutil.ReadFile(path)
92 if err != nil {
93 return err
94 }
95 h := sha256.New()
96 h.Write(b)
97 if hex.EncodeToString(h.Sum(nil)) != digest {
98 return fmt.Errorf("bad SHA256 of %q", path)
99 }
100 return nil
101}
102
103// Get returns the charm referenced by curl.
104func (s *store) Get(curl *URL) (Charm, error) {
105 if err := os.MkdirAll(s.cachePath, 0755); err != nil {
106 return nil, err
107 }
108 rev, digest, err := s.info(curl)
109 if err != nil {
110 return nil, err
111 }
112 if curl.Revision == -1 {
113 curl = curl.WithRevision(rev)
114 } else if curl.Revision != rev {
115 return nil, fmt.Errorf("charm: store returned charm with wrong revision for %q", curl.String())
116 }
117 path := filepath.Join(s.cachePath, Quote(curl.String())+".charm")
118 if verify(path, digest) != nil {
119 resp, err := http.Get(s.baseURL + "/charm/" + url.QueryEscape(curl.Path()))
120 if err != nil {
121 return nil, err
122 }
123 defer resp.Body.Close()
124 f, err := ioutil.TempFile(s.cachePath, "charm-download")
125 if err != nil {
126 return nil, err
127 }
128 dlPath := f.Name()
129 _, err = io.Copy(f, resp.Body)
130 if cerr := f.Close(); err == nil {
131 err = cerr
132 }
133 if err != nil {
134 os.Remove(dlPath)
135 return nil, err
136 }
137 if err := os.Rename(dlPath, path); err != nil {
138 return nil, err
139 }
140 }
141 if err := verify(path, digest); err != nil {
142 return nil, err
143 }
144 return ReadBundle(path)
145}
146
147// LocalRepository represents a local directory containing subdirectories
148// named after an Ubuntu series, each of which contains charms targeted for
149// that series. For example:
150//
151// /path/to/repository/oneiric/mongodb/
152// /path/to/repository/precise/mongodb.charm
153// /path/to/repository/precise/wordpress/
154type LocalRepository struct {
155 Path string
156}
157
158// Latest returns the latest revision of the charm referenced by curl, regardless
159// of the revision set on curl itself.
160func (r *LocalRepository) Latest(curl *URL) (int, error) {
161 ch, err := r.Get(curl.WithRevision(-1))
162 if err != nil {
163 return 0, err
164 }
165 return ch.Revision(), nil
166}
167
168func repoNotFound(path string) error {
169 return fmt.Errorf("no repository found at %q", path)
170}
171
172func charmNotFound(curl *URL) error {
173 return fmt.Errorf("no charms found matching %q", curl)
174}
175
176func mightBeCharm(info os.FileInfo) bool {
177 if info.IsDir() {
178 return !strings.HasPrefix(info.Name(), ".")
179 }
180 return strings.HasSuffix(info.Name(), ".charm")
181}
182
183// Get returns a charm matching curl, if one exists. If curl has a revision of
184// -1, it returns the latest charm that matches curl. If multiple candidates
185// satisfy the foregoing, the first one encountered will be returned.
186func (r *LocalRepository) Get(curl *URL) (Charm, error) {
187 if curl.Schema != "local" {
188 return nil, fmt.Errorf("local repository got URL with non-local schema: %q", curl)
189 }
190 info, err := os.Stat(r.Path)
191 if err != nil {
192 if os.IsNotExist(err) {
193 err = repoNotFound(r.Path)
194 }
195 return nil, err
196 }
197 if !info.IsDir() {
198 return nil, repoNotFound(r.Path)
199 }
200 path := filepath.Join(r.Path, curl.Series)
201 infos, err := ioutil.ReadDir(path)
202 if err != nil {
203 return nil, charmNotFound(curl)
204 }
205 var latest Charm
206 for _, info := range infos {
207 if !mightBeCharm(info) {
208 continue
209 }
210 chPath := filepath.Join(path, info.Name())
211 if ch, err := Read(chPath); err != nil {
212 log.Printf("WARNING: failed to load charm at %q: %s", chPath, err)
213 } else if ch.Meta().Name == curl.Name {
214 if ch.Revision() == curl.Revision {
215 return ch, nil
216 }
217 if latest == nil || ch.Revision() > latest.Revision() {
218 latest = ch
219 }
220 }
221 }
222 if curl.Revision == -1 && latest != nil {
223 return latest, nil
224 }
225 return nil, charmNotFound(curl)
226}
0227
=== added file 'charm/repo_test.go.OTHER'
--- charm/repo_test.go.OTHER 1970-01-01 00:00:00 +0000
+++ charm/repo_test.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,339 @@
1package charm_test
2
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "encoding/json"
7 "io/ioutil"
8 . "launchpad.net/gocheck"
9 "launchpad.net/juju-core/juju/charm"
10 "launchpad.net/juju-core/juju/log"
11 "launchpad.net/juju-core/juju/testing"
12 "net"
13 "net/http"
14 "os"
15 "path/filepath"
16 "strconv"
17)
18
19type MockStore struct {
20 mux *http.ServeMux
21 lis net.Listener
22 bundleBytes []byte
23 bundleSha256 string
24 downloads []*charm.URL
25}
26
27func NewMockStore(c *C) *MockStore {
28 s := &MockStore{}
29 bytes, err := ioutil.ReadFile(testing.Charms.BundlePath(c.MkDir(), "dummy"))
30 c.Assert(err, IsNil)
31 s.bundleBytes = bytes
32 h := sha256.New()
33 h.Write(bytes)
34 s.bundleSha256 = hex.EncodeToString(h.Sum(nil))
35 s.mux = http.NewServeMux()
36 s.mux.HandleFunc("/charm-info", func(w http.ResponseWriter, r *http.Request) {
37 s.ServeInfo(w, r)
38 })
39 s.mux.HandleFunc("/charm/", func(w http.ResponseWriter, r *http.Request) {
40 s.ServeCharm(w, r)
41 })
42 lis, err := net.Listen("tcp", "127.0.0.1:4444")
43 c.Assert(err, IsNil)
44 s.lis = lis
45 go http.Serve(s.lis, s)
46 return s
47}
48
49func (s *MockStore) ServeHTTP(w http.ResponseWriter, r *http.Request) {
50 s.mux.ServeHTTP(w, r)
51}
52
53func (s *MockStore) ServeInfo(w http.ResponseWriter, r *http.Request) {
54 r.ParseForm()
55 response := map[string]*charm.InfoResponse{}
56 for _, url := range r.Form["charms"] {
57 cr := &charm.InfoResponse{}
58 response[url] = cr
59 curl := charm.MustParseURL(url)
60 switch curl.Name {
61 case "borken":
62 cr.Errors = append(cr.Errors, "badness")
63 continue
64 case "unwise":
65 cr.Warnings = append(cr.Warnings, "foolishness")
66 fallthrough
67 default:
68 if curl.Revision == -1 {
69 cr.Revision = 23
70 } else {
71 cr.Revision = curl.Revision
72 }
73 cr.Sha256 = s.bundleSha256
74 }
75 }
76 data, err := json.Marshal(response)
77 if err != nil {
78 panic(err)
79 }
80 w.Header().Set("Content-Type", "application/json")
81 _, err = w.Write(data)
82 if err != nil {
83 panic(err)
84 }
85}
86
87func (s *MockStore) ServeCharm(w http.ResponseWriter, r *http.Request) {
88 curl := charm.MustParseURL("cs:" + r.URL.Path[len("/charm/"):])
89 s.downloads = append(s.downloads, curl)
90 w.Header().Set("Connection", "close")
91 w.Header().Set("Content-Type", "application/octet-stream")
92 w.Header().Set("Content-Length", strconv.Itoa(len(s.bundleBytes)))
93 _, err := w.Write(s.bundleBytes)
94 if err != nil {
95 panic(err)
96 }
97}
98
99type StoreSuite struct {
100 server *MockStore
101 store charm.Repository
102 cache string
103}
104
105var _ = Suite(&StoreSuite{})
106
107func (s *StoreSuite) SetUpSuite(c *C) {
108 s.server = NewMockStore(c)
109}
110
111func (s *StoreSuite) SetUpTest(c *C) {
112 s.cache = c.MkDir()
113 s.store = charm.NewStore("http://127.0.0.1:4444", s.cache)
114 s.server.downloads = nil
115}
116
117func (s *StoreSuite) TearDownSuite(c *C) {
118 s.server.lis.Close()
119}
120
121func (s *StoreSuite) TestError(c *C) {
122 curl := charm.MustParseURL("cs:series/borken")
123 expect := `charm info errors for "cs:series/borken": badness`
124 _, err := s.store.Latest(curl)
125 c.Assert(err, ErrorMatches, expect)
126 _, err = s.store.Get(curl)
127 c.Assert(err, ErrorMatches, expect)
128}
129
130func (s *StoreSuite) TestWarning(c *C) {
131 orig := log.Target
132 log.Target = c
133 defer func() { log.Target = orig }()
134 curl := charm.MustParseURL("cs:series/unwise")
135 expect := `.* JUJU WARNING: charm store reports for "cs:series/unwise": foolishness` + "\n"
136 r, err := s.store.Latest(curl)
137 c.Assert(r, Equals, 23)
138 c.Assert(err, IsNil)
139 c.Assert(c.GetTestLog(), Matches, expect)
140 ch, err := s.store.Get(curl)
141 c.Assert(ch, NotNil)
142 c.Assert(err, IsNil)
143 c.Assert(c.GetTestLog(), Matches, expect+expect)
144}
145
146func (s *StoreSuite) TestLatest(c *C) {
147 for _, str := range []string{
148 "cs:series/blah",
149 "cs:series/blah-2",
150 "cs:series/blah-99",
151 } {
152 r, err := s.store.Latest(charm.MustParseURL(str))
153 c.Assert(r, Equals, 23)
154 c.Assert(err, IsNil)
155 }
156}
157
158func (s *StoreSuite) assertCached(c *C, curl *charm.URL) {
159 s.server.downloads = nil
160 ch, err := s.store.Get(curl)
161 c.Assert(err, IsNil)
162 c.Assert(ch, NotNil)
163 c.Assert(s.server.downloads, IsNil)
164}
165
166func (s *StoreSuite) TestGetCacheImplicitRevision(c *C) {
167 os.RemoveAll(s.cache)
168 base := "cs:series/blah"
169 curl := charm.MustParseURL(base)
170 revCurl := charm.MustParseURL(base + "-23")
171 ch, err := s.store.Get(curl)
172 c.Assert(err, IsNil)
173 c.Assert(ch, NotNil)
174 c.Assert(s.server.downloads, DeepEquals, []*charm.URL{revCurl})
175 s.assertCached(c, curl)
176 s.assertCached(c, revCurl)
177}
178
179func (s *StoreSuite) TestGetCacheExplicitRevision(c *C) {
180 os.RemoveAll(s.cache)
181 base := "cs:series/blah-12"
182 curl := charm.MustParseURL(base)
183 ch, err := s.store.Get(curl)
184 c.Assert(err, IsNil)
185 c.Assert(ch, NotNil)
186 c.Assert(s.server.downloads, DeepEquals, []*charm.URL{curl})
187 s.assertCached(c, curl)
188}
189
190func (s *StoreSuite) TestGetBadCache(c *C) {
191 base := "cs:series/blah"
192 curl := charm.MustParseURL(base)
193 revCurl := charm.MustParseURL(base + "-23")
194 name := charm.Quote(revCurl.String()) + ".charm"
195 err := ioutil.WriteFile(filepath.Join(s.cache, name), nil, 0666)
196 c.Assert(err, IsNil)
197 ch, err := s.store.Get(curl)
198 c.Assert(err, IsNil)
199 c.Assert(ch, NotNil)
200 c.Assert(s.server.downloads, DeepEquals, []*charm.URL{revCurl})
201 s.assertCached(c, curl)
202 s.assertCached(c, revCurl)
203}
204
205type LocalRepoSuite struct {
206 testing.LoggingSuite
207 repo *charm.LocalRepository
208 seriesPath string
209}
210
211var _ = Suite(&LocalRepoSuite{})
212
213func (s *LocalRepoSuite) SetUpTest(c *C) {
214 s.LoggingSuite.SetUpTest(c)
215 root := c.MkDir()
216 s.repo = &charm.LocalRepository{root}
217 s.seriesPath = filepath.Join(root, "series")
218 c.Assert(os.Mkdir(s.seriesPath, 0777), IsNil)
219}
220
221func (s *LocalRepoSuite) addBundle(name string) string {
222 return testing.Charms.BundlePath(s.seriesPath, name)
223}
224
225func (s *LocalRepoSuite) addDir(name string) string {
226 return testing.Charms.ClonedDirPath(s.seriesPath, name)
227}
228
229func (s *LocalRepoSuite) TestMissingCharm(c *C) {
230 _, err := s.repo.Latest(charm.MustParseURL("local:series/zebra"))
231 c.Assert(err, ErrorMatches, `no charms found matching "local:series/zebra"`)
232 _, err = s.repo.Get(charm.MustParseURL("local:series/zebra"))
233 c.Assert(err, ErrorMatches, `no charms found matching "local:series/zebra"`)
234 _, err = s.repo.Latest(charm.MustParseURL("local:badseries/zebra"))
235 c.Assert(err, ErrorMatches, `no charms found matching "local:badseries/zebra"`)
236 _, err = s.repo.Get(charm.MustParseURL("local:badseries/zebra"))
237 c.Assert(err, ErrorMatches, `no charms found matching "local:badseries/zebra"`)
238}
239
240func (s *LocalRepoSuite) TestMissingRepo(c *C) {
241 c.Assert(os.RemoveAll(s.repo.Path), IsNil)
242 _, err := s.repo.Latest(charm.MustParseURL("local:series/zebra"))
243 c.Assert(err, ErrorMatches, `no repository found at ".*"`)
244 _, err = s.repo.Get(charm.MustParseURL("local:series/zebra"))
245 c.Assert(err, ErrorMatches, `no repository found at ".*"`)
246 c.Assert(ioutil.WriteFile(s.repo.Path, nil, 0666), IsNil)
247 _, err = s.repo.Latest(charm.MustParseURL("local:series/zebra"))
248 c.Assert(err, ErrorMatches, `no repository found at ".*"`)
249 _, err = s.repo.Get(charm.MustParseURL("local:series/zebra"))
250 c.Assert(err, ErrorMatches, `no repository found at ".*"`)
251}
252
253func (s *LocalRepoSuite) TestMultipleVersions(c *C) {
254 curl := charm.MustParseURL("local:series/sample")
255 s.addDir("old")
256 rev, err := s.repo.Latest(curl)
257 c.Assert(err, IsNil)
258 c.Assert(rev, Equals, 1)
259 ch, err := s.repo.Get(curl)
260 c.Assert(err, IsNil)
261 c.Assert(ch.Revision(), Equals, 1)
262
263 s.addDir("new")
264 rev, err = s.repo.Latest(curl)
265 c.Assert(err, IsNil)
266 c.Assert(rev, Equals, 2)
267 ch, err = s.repo.Get(curl)
268 c.Assert(err, IsNil)
269 c.Assert(ch.Revision(), Equals, 2)
270
271 revCurl := curl.WithRevision(1)
272 rev, err = s.repo.Latest(revCurl)
273 c.Assert(err, IsNil)
274 c.Assert(rev, Equals, 2)
275 ch, err = s.repo.Get(revCurl)
276 c.Assert(err, IsNil)
277 c.Assert(ch.Revision(), Equals, 1)
278
279 badRevCurl := curl.WithRevision(33)
280 rev, err = s.repo.Latest(badRevCurl)
281 c.Assert(err, IsNil)
282 c.Assert(rev, Equals, 2)
283 ch, err = s.repo.Get(badRevCurl)
284 c.Assert(err, ErrorMatches, `no charms found matching "local:series/sample-33"`)
285}
286
287func (s *LocalRepoSuite) TestBundle(c *C) {
288 curl := charm.MustParseURL("local:series/dummy")
289 s.addBundle("dummy")
290
291 rev, err := s.repo.Latest(curl)
292 c.Assert(err, IsNil)
293 c.Assert(rev, Equals, 1)
294 ch, err := s.repo.Get(curl)
295 c.Assert(err, IsNil)
296 c.Assert(ch.Revision(), Equals, 1)
297}
298
299func (s *LocalRepoSuite) TestLogsErrors(c *C) {
300 err := ioutil.WriteFile(filepath.Join(s.seriesPath, "blah.charm"), nil, 0666)
301 c.Assert(err, IsNil)
302 err = os.Mkdir(filepath.Join(s.seriesPath, "blah"), 0666)
303 c.Assert(err, IsNil)
304 samplePath := s.addDir("new")
305 gibberish := []byte("don't parse me by")
306 err = ioutil.WriteFile(filepath.Join(samplePath, "metadata.yaml"), gibberish, 0666)
307 c.Assert(err, IsNil)
308
309 curl := charm.MustParseURL("local:series/dummy")
310 s.addDir("dummy")
311 ch, err := s.repo.Get(curl)
312 c.Assert(err, IsNil)
313 c.Assert(ch.Revision(), Equals, 1)
314 c.Assert(c.GetTestLog(), Matches, `
315.* JUJU WARNING: failed to load charm at ".*/series/blah": .*
316.* JUJU WARNING: failed to load charm at ".*/series/blah.charm": .*
317.* JUJU WARNING: failed to load charm at ".*/series/new": .*
318`[1:])
319}
320
321func renameSibling(c *C, path, name string) {
322 c.Assert(os.Rename(path, filepath.Join(filepath.Dir(path), name)), IsNil)
323}
324
325func (s *LocalRepoSuite) TestIgnoresUnpromisingNames(c *C) {
326 err := ioutil.WriteFile(filepath.Join(s.seriesPath, "blah.notacharm"), nil, 0666)
327 c.Assert(err, IsNil)
328 err = os.Mkdir(filepath.Join(s.seriesPath, ".blah"), 0666)
329 c.Assert(err, IsNil)
330 renameSibling(c, s.addDir("dummy"), ".dummy")
331 renameSibling(c, s.addBundle("dummy"), "dummy.notacharm")
332 curl := charm.MustParseURL("local:series/dummy")
333
334 _, err = s.repo.Get(curl)
335 c.Assert(err, ErrorMatches, `no charms found matching "local:series/dummy"`)
336 _, err = s.repo.Latest(curl)
337 c.Assert(err, ErrorMatches, `no charms found matching "local:series/dummy"`)
338 c.Assert(c.GetTestLog(), Equals, "")
339}
0340
=== added file 'charm/url_test.go.OTHER'
--- charm/url_test.go.OTHER 1970-01-01 00:00:00 +0000
+++ charm/url_test.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,126 @@
1package charm_test
2
3import (
4 "fmt"
5 . "launchpad.net/gocheck"
6 "launchpad.net/juju-core/juju/charm"
7)
8
9type URLSuite struct{}
10
11var _ = Suite(&URLSuite{})
12
13var urlTests = []struct {
14 s, err string
15 url *charm.URL
16}{
17 {"cs:~user/series/name", "", &charm.URL{"cs", "user", "series", "name", -1}},
18 {"cs:~user/series/name-0", "", &charm.URL{"cs", "user", "series", "name", 0}},
19 {"cs:series/name", "", &charm.URL{"cs", "", "series", "name", -1}},
20 {"cs:series/name-42", "", &charm.URL{"cs", "", "series", "name", 42}},
21 {"local:series/name-1", "", &charm.URL{"local", "", "series", "name", 1}},
22 {"local:series/name", "", &charm.URL{"local", "", "series", "name", -1}},
23 {"local:series/n0-0n-n0", "", &charm.URL{"local", "", "series", "n0-0n-n0", -1}},
24
25 {"bs:~user/series/name-1", "charm URL has invalid schema: .*", nil},
26 {"cs:~1/series/name-1", "charm URL has invalid user name: .*", nil},
27 {"cs:~user/1/name-1", "charm URL has invalid series: .*", nil},
28 {"cs:~user/series/name-1-2", "charm URL has invalid charm name: .*", nil},
29 {"cs:~user/series/name-1-name-2", "charm URL has invalid charm name: .*", nil},
30 {"cs:~user/series/name--name-2", "charm URL has invalid charm name: .*", nil},
31 {"cs:~user/series/huh/name-1", "charm URL has invalid form: .*", nil},
32 {"cs:~user/name", "charm URL without series: .*", nil},
33 {"cs:name", "charm URL without series: .*", nil},
34 {"local:~user/series/name", "local charm URL with user name: .*", nil},
35 {"local:~user/name", "local charm URL with user name: .*", nil},
36 {"local:name", "charm URL without series: .*", nil},
37}
38
39func (s *URLSuite) TestParseURL(c *C) {
40 for _, t := range urlTests {
41 url, err := charm.ParseURL(t.s)
42 comment := Commentf("ParseURL(%q)", t.s)
43 if t.err != "" {
44 c.Check(err.Error(), Matches, t.err, comment)
45 } else {
46 c.Check(url, DeepEquals, t.url, comment)
47 c.Check(t.url.String(), Equals, t.s)
48 }
49 }
50}
51
52var inferTests = []struct {
53 vague, exact string
54}{
55 {"foo", "cs:defseries/foo"},
56 {"foo-1", "cs:defseries/foo-1"},
57 {"n0-n0-n0", "cs:defseries/n0-n0-n0"},
58 {"cs:foo", "cs:defseries/foo"},
59 {"local:foo", "local:defseries/foo"},
60 {"series/foo", "cs:series/foo"},
61 {"cs:series/foo", "cs:series/foo"},
62 {"local:series/foo", "local:series/foo"},
63 {"cs:~user/foo", "cs:~user/defseries/foo"},
64 {"cs:~user/series/foo", "cs:~user/series/foo"},
65 {"local:~user/series/foo", "local:~user/series/foo"},
66 {"bs:foo", "bs:defseries/foo"},
67 {"cs:~1/foo", "cs:~1/defseries/foo"},
68 {"cs:foo-1-2", "cs:defseries/foo-1-2"},
69}
70
71func (s *URLSuite) TestInferURL(c *C) {
72 for _, t := range inferTests {
73 comment := Commentf("InferURL(%q, %q)", t.vague, "defseries")
74 inferred, ierr := charm.InferURL(t.vague, "defseries")
75 parsed, perr := charm.ParseURL(t.exact)
76 if parsed != nil {
77 c.Check(inferred, DeepEquals, parsed, comment)
78 } else {
79 expect := perr.Error()
80 if t.vague != t.exact {
81 expect = fmt.Sprintf("%s (URL inferred from %q)", expect, t.vague)
82 }
83 c.Check(ierr.Error(), Equals, expect, comment)
84 }
85 }
86 u, err := charm.InferURL("~blah", "defseries")
87 c.Assert(u, IsNil)
88 c.Assert(err, ErrorMatches, "cannot infer charm URL with user but no schema: .*")
89}
90
91func (s *URLSuite) TestMustParseURL(c *C) {
92 url := charm.MustParseURL("cs:series/name")
93 c.Assert(url, DeepEquals, &charm.URL{"cs", "", "series", "name", -1})
94 f := func() { charm.MustParseURL("local:name") }
95 c.Assert(f, PanicMatches, "charm URL without series: .*")
96}
97
98func (s *URLSuite) TestWithRevision(c *C) {
99 url := charm.MustParseURL("cs:series/name")
100 other := url.WithRevision(1)
101 c.Assert(url, DeepEquals, &charm.URL{"cs", "", "series", "name", -1})
102 c.Assert(other, DeepEquals, &charm.URL{"cs", "", "series", "name", 1})
103
104 // Should always copy. The opposite behavior is error prone.
105 c.Assert(other.WithRevision(1), Not(Equals), other)
106 c.Assert(other.WithRevision(1), DeepEquals, other)
107}
108
109type QuoteSuite struct{}
110
111var _ = Suite(&QuoteSuite{})
112
113func (s *QuoteSuite) TestUnmodified(c *C) {
114 // Check that a string containing only valid
115 // chars stays unmodified.
116 in := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-"
117 out := charm.Quote(in)
118 c.Assert(out, Equals, in)
119}
120
121func (s *QuoteSuite) TestQuote(c *C) {
122 // Check that invalid chars are translated correctly.
123 in := "hello_there/how'are~you-today.sir"
124 out := charm.Quote(in)
125 c.Assert(out, Equals, "hello_5f_there_2f_how_27_are_7e_you-today.sir")
126}
0127
=== added directory 'cloudinit'
=== added file 'cloudinit/cloudinit_test.go.OTHER'
--- cloudinit/cloudinit_test.go.OTHER 1970-01-01 00:00:00 +0000
+++ cloudinit/cloudinit_test.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,228 @@
1package cloudinit_test
2
3import (
4 "fmt"
5 . "launchpad.net/gocheck"
6 "launchpad.net/juju-core/juju/cloudinit"
7 "testing"
8)
9
10// TODO integration tests, but how?
11
12type S struct{}
13
14var _ = Suite(S{})
15
16func Test1(t *testing.T) {
17 TestingT(t)
18}
19
20var ctests = []struct {
21 name string
22 expect string
23 setOption func(cfg *cloudinit.Config)
24}{
25 {
26 "User",
27 "user: me\n",
28 func(cfg *cloudinit.Config) {
29 cfg.SetUser("me")
30 },
31 },
32 {
33 "AptUpgrade",
34 "apt_upgrade: true\n",
35 func(cfg *cloudinit.Config) {
36 cfg.SetAptUpgrade(true)
37 },
38 },
39 {
40 "AptUpdate",
41 "apt_update: true\n",
42 func(cfg *cloudinit.Config) {
43 cfg.SetAptUpdate(true)
44 },
45 },
46 {
47 "AptMirror",
48 "apt_mirror: http://foo.com\n",
49 func(cfg *cloudinit.Config) {
50 cfg.SetAptMirror("http://foo.com")
51 },
52 },
53 {
54 "AptPreserveSourcesList",
55 "apt_mirror: true\n",
56 func(cfg *cloudinit.Config) {
57 cfg.SetAptPreserveSourcesList(true)
58 },
59 },
60 {
61 "DebconfSelections",
62 "debconf_selections: '# Force debconf priority to critical.\n\n debconf debconf/priority select critical\n\n'\n",
63 func(cfg *cloudinit.Config) {
64 cfg.SetDebconfSelections("# Force debconf priority to critical.\ndebconf debconf/priority select critical\n")
65 },
66 },
67 {
68 "DisableEC2Metadata",
69 "disable_ec2_metadata: true\n",
70 func(cfg *cloudinit.Config) {
71 cfg.SetDisableEC2Metadata(true)
72 },
73 },
74 {
75 "FinalMessage",
76 "final_message: goodbye\n",
77 func(cfg *cloudinit.Config) {
78 cfg.SetFinalMessage("goodbye")
79 },
80 },
81 {
82 "Locale",
83 "locale: en_us\n",
84 func(cfg *cloudinit.Config) {
85 cfg.SetLocale("en_us")
86 },
87 },
88 {
89 "DisableRoot",
90 "disable_root: false\n",
91 func(cfg *cloudinit.Config) {
92 cfg.SetDisableRoot(false)
93 },
94 },
95 {
96 "SSHAuthorizedKeys",
97 "ssh_authorized_keys:\n- key1\n- key2\n",
98 func(cfg *cloudinit.Config) {
99 cfg.AddSSHAuthorizedKeys("key1")
100 cfg.AddSSHAuthorizedKeys("key2")
101 },
102 },
103 {
104 "SSHAuthorizedKeys",
105 "ssh_authorized_keys:\n- key1\n- key2\n- key3\n",
106 func(cfg *cloudinit.Config) {
107 cfg.AddSSHAuthorizedKeys("#command\nkey1")
108 cfg.AddSSHAuthorizedKeys("key2\n# comment\n\nkey3\n")
109 cfg.AddSSHAuthorizedKeys("")
110 },
111 },
112 {
113 "SSHKeys RSAPrivate",
114 "ssh_keys:\n rsa_private: key1data\n",
115 func(cfg *cloudinit.Config) {
116 cfg.AddSSHKey(cloudinit.RSAPrivate, "key1data")
117 },
118 },
119 {
120 "SSHKeys RSAPublic",
121 "ssh_keys:\n rsa_public: key2data\n",
122 func(cfg *cloudinit.Config) {
123 cfg.AddSSHKey(cloudinit.RSAPublic, "key2data")
124 },
125 },
126 {
127 "SSHKeys DSAPublic",
128 "ssh_keys:\n dsa_public: key1data\n",
129 func(cfg *cloudinit.Config) {
130 cfg.AddSSHKey(cloudinit.DSAPublic, "key1data")
131 },
132 },
133 {
134 "SSHKeys DSAPrivate",
135 "ssh_keys:\n dsa_private: key2data\n",
136 func(cfg *cloudinit.Config) {
137 cfg.AddSSHKey(cloudinit.DSAPrivate, "key2data")
138 },
139 },
140 {
141 "Output",
142 "output:\n all:\n - '>foo'\n - '|bar'\n",
143 func(cfg *cloudinit.Config) {
144 cfg.SetOutput("all", ">foo", "|bar")
145 },
146 },
147 {
148 "Output",
149 "output:\n all: '>foo'\n",
150 func(cfg *cloudinit.Config) {
151 cfg.SetOutput(cloudinit.OutAll, ">foo", "")
152 },
153 },
154 {
155 "AptSources",
156 "apt_sources:\n- source: keyName\n key: someKey\n",
157 func(cfg *cloudinit.Config) {
158 cfg.AddAptSource("keyName", "someKey")
159 },
160 },
161 {
162 "AptSources",
163 "apt_sources:\n- source: keyName\n keyid: someKey\n keyserver: foo.com\n",
164 func(cfg *cloudinit.Config) {
165 cfg.AddAptSourceWithKeyId("keyName", "someKey", "foo.com")
166 },
167 },
168 {
169 "Packages",
170 "packages:\n- juju\n- ubuntu\n",
171 func(cfg *cloudinit.Config) {
172 cfg.AddPackage("juju")
173 cfg.AddPackage("ubuntu")
174 },
175 },
176 {
177 "BootCmd",
178 "bootcmd:\n- ls > /dev\n- - ls\n - '>with space'\n",
179 func(cfg *cloudinit.Config) {
180 cfg.AddBootCmd("ls > /dev")
181 cfg.AddBootCmdArgs("ls", ">with space")
182 },
183 },
184 {
185 "Mounts",
186 "mounts:\n- - x\n - \"y\"\n- - z\n - w\n",
187 func(cfg *cloudinit.Config) {
188 cfg.AddMount("x", "y")
189 cfg.AddMount("z", "w")
190 },
191 },
192 {
193 "Attr",
194 "arbitraryAttr: someValue\n",
195 func(cfg *cloudinit.Config) {
196 cfg.SetAttr("arbitraryAttr", "someValue")
197 },
198 },
199}
200
201const header = "#cloud-config\n"
202
203func (S) TestOutput(c *C) {
204 for _, t := range ctests {
205 cfg := cloudinit.New()
206 t.setOption(cfg)
207 data, err := cfg.Render()
208 c.Assert(err, IsNil)
209 c.Assert(data, NotNil)
210 c.Assert(string(data), Equals, header+t.expect, Commentf("test %q output differs", t.name))
211 }
212}
213
214//#cloud-config
215//packages:
216//- juju
217//- ubuntu
218func ExampleConfig() {
219 cfg := cloudinit.New()
220 cfg.AddPackage("juju")
221 cfg.AddPackage("ubuntu")
222 data, err := cfg.Render()
223 if err != nil {
224 fmt.Printf("render error: %v", err)
225 return
226 }
227 fmt.Printf("%s", data)
228}
0229
=== added directory 'cmd'
=== added file 'cmd/cmd.go.OTHER'
--- cmd/cmd.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/cmd.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,139 @@
1package cmd
2
3import (
4 "bytes"
5 "errors"
6 "fmt"
7 "io"
8 "io/ioutil"
9 "launchpad.net/gnuflag"
10 "launchpad.net/juju-core/juju/log"
11 "os"
12 "path/filepath"
13 "strings"
14)
15
16// ErrSilent can be returned from Run to signal that Main should exit with
17// code 1 without producing error output.
18var ErrSilent = errors.New("cmd: error out silently")
19
20// Command is implemented by types that interpret command-line arguments.
21type Command interface {
22 // Info returns information about the Command.
23 Info() *Info
24
25 // Init initializes the Command before running. The command may add options
26 // to f before processing args.
27 Init(f *gnuflag.FlagSet, args []string) error
28
29 // Run will execute the Command as directed by the options and positional
30 // arguments passed to Init.
31 Run(ctx *Context) error
32}
33
34// Context represents the run context of a Command. Command implementations
35// should interpret file names relative to Dir (see AbsPath below), and print
36// output and errors to Stdout and Stderr respectively.
37type Context struct {
38 Dir string
39 Stdout io.Writer
40 Stderr io.Writer
41}
42
43// AbsPath returns an absolute representation of path, with relative paths
44// interpreted as relative to ctx.Dir.
45func (ctx *Context) AbsPath(path string) string {
46 if filepath.IsAbs(path) {
47 return path
48 }
49 return filepath.Join(ctx.Dir, path)
50}
51
52// Info holds some of the usage documentation of a Command.
53type Info struct {
54 // Name is the Command's name.
55 Name string
56
57 // Args describes the command's expected positional arguments.
58 Args string
59
60 // Purpose is a short explanation of the Command's purpose.
61 Purpose string
62
63 // Doc is the long documentation for the Command.
64 Doc string
65}
66
67// Help renders i's content, along with documentation for any
68// flags defined in f. It calls f.SetOutput(ioutil.Discard).
69func (i *Info) Help(f *gnuflag.FlagSet) []byte {
70 buf := &bytes.Buffer{}
71 fmt.Fprintf(buf, "usage: %s", i.Name)
72 hasOptions := false
73 f.VisitAll(func(f *gnuflag.Flag) { hasOptions = true })
74 if hasOptions {
75 fmt.Fprintf(buf, " [options]")
76 }
77 if i.Args != "" {
78 fmt.Fprintf(buf, " %s", i.Args)
79 }
80 fmt.Fprintf(buf, "\n")
81 if i.Purpose != "" {
82 fmt.Fprintf(buf, "purpose: %s\n", i.Purpose)
83 }
84 if hasOptions {
85 fmt.Fprintf(buf, "\noptions:\n")
86 f.SetOutput(buf)
87 f.PrintDefaults()
88 }
89 f.SetOutput(ioutil.Discard)
90 if i.Doc != "" {
91 fmt.Fprintf(buf, "\n%s\n", strings.TrimSpace(i.Doc))
92 }
93 return buf.Bytes()
94}
95
96// Main runs the given Command in the supplied Context with the given
97// arguments, which should not include the command name. It returns a code
98// suitable for passing to os.Exit.
99func Main(c Command, ctx *Context, args []string) int {
100 f := gnuflag.NewFlagSet(c.Info().Name, gnuflag.ContinueOnError)
101 f.SetOutput(ioutil.Discard)
102 if err := c.Init(f, args); err != nil {
103 ctx.Stderr.Write(c.Info().Help(f))
104 if err == gnuflag.ErrHelp {
105 return 0
106 }
107 fmt.Fprintf(ctx.Stderr, "error: %v\n", err)
108 return 2
109 }
110 if err := c.Run(ctx); err != nil {
111 if err != ErrSilent {
112 log.Debugf("%s command failed: %s\n", c.Info().Name, err)
113 fmt.Fprintf(ctx.Stderr, "error: %v\n", err)
114 }
115 return 1
116 }
117 return 0
118}
119
120// DefaultContext returns a Context suitable for use in non-hosted situations.
121func DefaultContext() *Context {
122 dir, err := os.Getwd()
123 if err != nil {
124 panic(err)
125 }
126 abs, err := filepath.Abs(dir)
127 if err != nil {
128 panic(err)
129 }
130 return &Context{abs, os.Stdout, os.Stderr}
131}
132
133// CheckEmpty is a utility function that returns an error if args is not empty.
134func CheckEmpty(args []string) error {
135 if len(args) != 0 {
136 return fmt.Errorf("unrecognized args: %q", args)
137 }
138 return nil
139}
0140
=== added file 'cmd/cmd_test.go.OTHER'
--- cmd/cmd_test.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/cmd_test.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,96 @@
1package cmd_test
2
3import (
4 . "launchpad.net/gocheck"
5 "launchpad.net/juju-core/juju/cmd"
6 "path/filepath"
7 "testing"
8)
9
10func Test(t *testing.T) { TestingT(t) }
11
12type CmdSuite struct{}
13
14var _ = Suite(&CmdSuite{})
15
16func (s *CmdSuite) TestContext(c *C) {
17 ctx := dummyContext(c)
18 c.Assert(ctx.AbsPath("/foo/bar"), Equals, "/foo/bar")
19 c.Assert(ctx.AbsPath("foo/bar"), Equals, filepath.Join(ctx.Dir, "foo/bar"))
20}
21
22func (s *CmdSuite) TestInfo(c *C) {
23 minimal := &TestCommand{Name: "verb", Minimal: true}
24 help := minimal.Info().Help(dummyFlagSet())
25 c.Assert(string(help), Equals, minimalHelp)
26
27 full := &TestCommand{Name: "verb"}
28 f := dummyFlagSet()
29 var ignored string
30 f.StringVar(&ignored, "option", "", "option-doc")
31 help = full.Info().Help(f)
32 c.Assert(string(help), Equals, fullHelp)
33
34 optionInfo := full.Info()
35 optionInfo.Doc = ""
36 help = optionInfo.Help(f)
37 c.Assert(string(help), Equals, optionHelp)
38}
39
40var initErrorTests = []struct {
41 c *TestCommand
42 help string
43}{
44 {&TestCommand{Name: "verb"}, fullHelp},
45 {&TestCommand{Name: "verb", Minimal: true}, minimalHelp},
46}
47
48func (s *CmdSuite) TestMainInitError(c *C) {
49 for _, t := range initErrorTests {
50 ctx := dummyContext(c)
51 result := cmd.Main(t.c, ctx, []string{"--unknown"})
52 c.Assert(result, Equals, 2)
53 c.Assert(bufferString(ctx.Stdout), Equals, "")
54 expected := t.help + "error: flag provided but not defined: --unknown\n"
55 c.Assert(bufferString(ctx.Stderr), Equals, expected)
56 }
57}
58
59func (s *CmdSuite) TestMainRunError(c *C) {
60 ctx := dummyContext(c)
61 result := cmd.Main(&TestCommand{Name: "verb"}, ctx, []string{"--option", "error"})
62 c.Assert(result, Equals, 1)
63 c.Assert(bufferString(ctx.Stdout), Equals, "")
64 c.Assert(bufferString(ctx.Stderr), Equals, "error: BAM!\n")
65}
66
67func (s *CmdSuite) TestMainRunSilentError(c *C) {
68 ctx := dummyContext(c)
69 result := cmd.Main(&TestCommand{Name: "verb"}, ctx, []string{"--option", "silent-error"})
70 c.Assert(result, Equals, 1)
71 c.Assert(bufferString(ctx.Stdout), Equals, "")
72 c.Assert(bufferString(ctx.Stderr), Equals, "")
73}
74
75func (s *CmdSuite) TestMainSuccess(c *C) {
76 ctx := dummyContext(c)
77 result := cmd.Main(&TestCommand{Name: "verb"}, ctx, []string{"--option", "success!"})
78 c.Assert(result, Equals, 0)
79 c.Assert(bufferString(ctx.Stdout), Equals, "success!\n")
80 c.Assert(bufferString(ctx.Stderr), Equals, "")
81}
82
83func (s *CmdSuite) TestMainHelp(c *C) {
84 for _, arg := range []string{"-h", "--help"} {
85 ctx := dummyContext(c)
86 result := cmd.Main(&TestCommand{Name: "verb"}, ctx, []string{arg})
87 c.Assert(result, Equals, 0)
88 c.Assert(bufferString(ctx.Stdout), Equals, "")
89 c.Assert(bufferString(ctx.Stderr), Equals, fullHelp)
90 }
91}
92
93func (s *CmdSuite) TestCheckEmpty(c *C) {
94 c.Assert(cmd.CheckEmpty(nil), IsNil)
95 c.Assert(cmd.CheckEmpty([]string{"boo!"}), ErrorMatches, `unrecognized args: \["boo!"\]`)
96}
097
=== added directory 'cmd/juju'
=== added file 'cmd/juju/bootstrap.go.OTHER'
--- cmd/juju/bootstrap.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/juju/bootstrap.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,37 @@
1package main
2
3import (
4 "launchpad.net/gnuflag"
5 "launchpad.net/juju-core/juju/cmd"
6 "launchpad.net/juju-core/juju/juju"
7)
8
9// BootstrapCommand is responsible for launching the first machine in a juju
10// environment, and setting up everything necessary to continue working.
11type BootstrapCommand struct {
12 EnvName string
13 UploadTools bool
14}
15
16func (c *BootstrapCommand) Info() *cmd.Info {
17 return &cmd.Info{"bootstrap", "", "start up an environment from scratch", ""}
18}
19
20func (c *BootstrapCommand) Init(f *gnuflag.FlagSet, args []string) error {
21 addEnvironFlags(&c.EnvName, f)
22 f.BoolVar(&c.UploadTools, "upload-tools", false, "upload local version of tools before bootstrapping")
23 if err := f.Parse(true, args); err != nil {
24 return err
25 }
26 return cmd.CheckEmpty(f.Args())
27}
28
29// Run connects to the environment specified on the command line and bootstraps
30// a juju in that environment if none already exists.
31func (c *BootstrapCommand) Run(_ *cmd.Context) error {
32 conn, err := juju.NewConn(c.EnvName)
33 if err != nil {
34 return err
35 }
36 return conn.Bootstrap(c.UploadTools)
37}
038
=== added file 'cmd/juju/cmd_test.go.OTHER'
--- cmd/juju/cmd_test.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/juju/cmd_test.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,255 @@
1package main
2
3import (
4 "io/ioutil"
5 "launchpad.net/gnuflag"
6 . "launchpad.net/gocheck"
7 "launchpad.net/juju-core/juju/cmd"
8 "launchpad.net/juju-core/juju/environs"
9 "launchpad.net/juju-core/juju/environs/dummy"
10 "launchpad.net/juju-core/juju/testing"
11 "os"
12 "path/filepath"
13 "reflect"
14)
15
16type cmdSuite struct {
17 testing.LoggingSuite
18 home string
19}
20
21var _ = Suite(&cmdSuite{})
22
23// N.B. Barking is broken.
24var config = `
25default:
26 peckham
27environments:
28 peckham:
29 type: dummy
30 zookeeper: false
31 walthamstow:
32 type: dummy
33 zookeeper: false
34 barking:
35 type: dummy
36 broken: true
37 zookeeper: false
38`
39
40func (s *cmdSuite) SetUpTest(c *C) {
41 s.LoggingSuite.SetUpTest(c)
42 // Arrange so that the "home" directory points
43 // to a temporary directory containing the config file.
44 s.home = os.Getenv("HOME")
45 dir := c.MkDir()
46 os.Setenv("HOME", dir)
47 err := os.Mkdir(filepath.Join(dir, ".juju"), 0777)
48 c.Assert(err, IsNil)
49 err = ioutil.WriteFile(filepath.Join(dir, ".juju", "environments.yaml"), []byte(config), 0666)
50 c.Assert(err, IsNil)
51}
52
53func (s *cmdSuite) TearDownTest(c *C) {
54 os.Setenv("HOME", s.home)
55
56 dummy.Reset()
57 s.LoggingSuite.TearDownTest(c)
58}
59
60func newFlagSet() *gnuflag.FlagSet {
61 return gnuflag.NewFlagSet("", gnuflag.ContinueOnError)
62}
63
64// testInit checks that a command initialises correctly
65// with the given set of arguments.
66func testInit(c *C, com cmd.Command, args []string, errPat string) {
67 err := com.Init(newFlagSet(), args)
68 if errPat != "" {
69 c.Assert(err, ErrorMatches, errPat)
70 } else {
71 c.Assert(err, IsNil)
72 }
73}
74
75// assertConnName asserts that the Command is using
76// the given environment name.
77// Since every command has a different type,
78// we use reflection to look at the value of the
79// Conn field in the value.
80func assertConnName(c *C, com cmd.Command, name string) {
81 v := reflect.ValueOf(com).Elem().FieldByName("EnvName")
82 c.Assert(v.IsValid(), Equals, true)
83 c.Assert(v.Interface(), Equals, name)
84}
85
86// All members of EnvironmentInitTests are tested for the -environment and -e
87// flags, and that extra arguments will cause parsing to fail.
88var EnvironmentInitTests = []func() (cmd.Command, []string){
89 func() (cmd.Command, []string) { return new(BootstrapCommand), nil },
90 func() (cmd.Command, []string) { return new(DestroyCommand), nil },
91 func() (cmd.Command, []string) {
92 return new(DeployCommand), []string{"charm-name", "service-name"}
93 },
94}
95
96// TestEnvironmentInit tests that all commands which accept
97// the --environment variable initialise their
98// environment name correctly.
99func (*cmdSuite) TestEnvironmentInit(c *C) {
100 for i, cmdFunc := range EnvironmentInitTests {
101 c.Logf("test %d", i)
102 com, args := cmdFunc()
103 testInit(c, com, args, "")
104 assertConnName(c, com, "")
105
106 com, args = cmdFunc()
107 testInit(c, com, append(args, "-e", "walthamstow"), "")
108 assertConnName(c, com, "walthamstow")
109
110 com, args = cmdFunc()
111 testInit(c, com, append(args, "--environment", "walthamstow"), "")
112 assertConnName(c, com, "walthamstow")
113
114 com, args = cmdFunc()
115 testInit(c, com, append(args, "hotdog"), "unrecognized args.*")
116 }
117}
118
119func runCommand(com cmd.Command, args ...string) (opc chan dummy.Operation, errc chan error) {
120 errc = make(chan error, 1)
121 opc = make(chan dummy.Operation)
122 dummy.Reset()
123 dummy.Listen(opc)
124 go func() {
125 // signal that we're done with this ops channel.
126 defer dummy.Listen(nil)
127
128 err := com.Init(newFlagSet(), args)
129 if err != nil {
130 errc <- err
131 return
132 }
133
134 err = com.Run(cmd.DefaultContext())
135 errc <- err
136 }()
137 return
138}
139
140func op(kind dummy.OperationKind, name string) dummy.Operation {
141 return dummy.Operation{
142 Env: name,
143 Kind: kind,
144 }
145}
146
147func (*cmdSuite) TestBootstrapCommand(c *C) {
148 // normal bootstrap
149 opc, errc := runCommand(new(BootstrapCommand))
150 c.Check(<-opc, Equals, op(dummy.OpBootstrap, "peckham"))
151 c.Check(<-errc, IsNil)
152
153 // bootstrap with tool uploading - checking that a file
154 // is uploaded should be sufficient, as the detailed semantics
155 // of UploadTools are tested in environs.
156 opc, errc = runCommand(new(BootstrapCommand), "--upload-tools")
157 c.Check(<-opc, Equals, op(dummy.OpPutFile, "peckham"))
158 c.Check(<-opc, Equals, op(dummy.OpBootstrap, "peckham"))
159 c.Check(<-errc, IsNil)
160
161 envs, err := environs.ReadEnvirons("")
162 c.Assert(err, IsNil)
163 env, err := envs.Open("peckham")
164 c.Assert(err, IsNil)
165 dir := c.MkDir()
166 err = environs.GetTools(env, dir)
167 c.Assert(err, IsNil)
168
169 // bootstrap with broken environment
170 opc, errc = runCommand(new(BootstrapCommand), "-e", "barking")
171 c.Check((<-opc).Kind, Equals, dummy.OpNone)
172 c.Check(<-errc, ErrorMatches, `broken environment`)
173}
174
175func (*cmdSuite) TestDestroyCommand(c *C) {
176 // normal destroy
177 opc, errc := runCommand(new(DestroyCommand))
178 c.Check(<-opc, Equals, op(dummy.OpDestroy, "peckham"))
179 c.Check(<-errc, IsNil)
180
181 // destroy with broken environment
182 opc, errc = runCommand(new(DestroyCommand), "-e", "barking")
183 c.Check((<-opc).Kind, Equals, dummy.OpNone)
184 c.Check(<-errc, ErrorMatches, `broken environment`)
185}
186
187var deployTests = []struct {
188 args []string
189 com *DeployCommand
190}{
191 {
192 []string{"charm-name"},
193 &DeployCommand{},
194 }, {
195 []string{"charm-name", "service-name"},
196 &DeployCommand{ServiceName: "service-name"},
197 }, {
198 []string{"--config", "/path/to/config.yaml", "charm-name"},
199 &DeployCommand{ConfPath: "/path/to/config.yaml"},
200 }, {
201 []string{"--repository", "/path/to/another-repo", "charm-name"},
202 &DeployCommand{RepoPath: "/path/to/another-repo"},
203 }, {
204 []string{"--upgrade", "charm-name"},
205 &DeployCommand{Upgrade: true},
206 }, {
207 []string{"-u", "charm-name"},
208 &DeployCommand{Upgrade: true},
209 }, {
210 []string{"--num-units", "33", "charm-name"},
211 &DeployCommand{NumUnits: 33},
212 }, {
213 []string{"-n", "104", "charm-name"},
214 &DeployCommand{NumUnits: 104},
215 },
216}
217
218func initExpectations(com *DeployCommand) {
219 if com.CharmName == "" {
220 com.CharmName = "charm-name"
221 }
222 if com.NumUnits == 0 {
223 com.NumUnits = 1
224 }
225 if com.RepoPath == "" {
226 com.RepoPath = "/path/to/repo"
227 }
228}
229
230func initDeployCommand(args ...string) (*DeployCommand, error) {
231 com := &DeployCommand{}
232 return com, com.Init(newFlagSet(), args)
233}
234
235func (*cmdSuite) TestDeployCommandInit(c *C) {
236 defer os.Setenv("JUJU_REPOSITORY", os.Getenv("JUJU_REPOSITORY"))
237 os.Setenv("JUJU_REPOSITORY", "/path/to/repo")
238
239 for _, t := range deployTests {
240 initExpectations(t.com)
241 com, err := initDeployCommand(t.args...)
242 c.Assert(err, IsNil)
243 c.Assert(com, DeepEquals, t.com)
244 }
245
246 // missing args
247 _, err := initDeployCommand()
248 c.Assert(err, ErrorMatches, "no charm specified")
249
250 // bad unit count
251 _, err = initDeployCommand("charm-name", "--num-units", "0")
252 c.Assert(err, ErrorMatches, "must deploy at least one unit")
253
254 // environment tested elsewhere
255}
0256
=== added file 'cmd/juju/deploy.go.OTHER'
--- cmd/juju/deploy.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/juju/deploy.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,79 @@
1package main
2
3import (
4 "errors"
5 "launchpad.net/gnuflag"
6 "launchpad.net/juju-core/juju/cmd"
7 "os"
8)
9
10type DeployCommand struct {
11 EnvName string
12 CharmName string
13 ServiceName string
14 ConfPath string
15 NumUnits int
16 Upgrade bool
17 RepoPath string // defaults to JUJU_REPOSITORY
18}
19
20const deployDoc = `
21<charm name> can be a charm URL, or an unambiguously condensed form of it;
22assuming a current default series of "precise", the following forms will be
23accepted.
24
25For cs:precise/mysql
26 mysql
27 precise/mysql
28
29For cs:~user/precise/mysql
30 cs:~user/mysql
31
32For local:precise/mysql
33 local:mysql
34
35In all cases, a versioned charm URL will be expanded as expected (for example,
36mysql-33 becomes cs:precise/mysql-33).
37
38<service name>, if omitted, will be derived from <charm name>.
39`
40
41func (c *DeployCommand) Info() *cmd.Info {
42 return &cmd.Info{
43 "deploy", "<charm name> [<service name>]", "deploy a new service", deployDoc,
44 }
45}
46
47func (c *DeployCommand) Init(f *gnuflag.FlagSet, args []string) error {
48 addEnvironFlags(&c.EnvName, f)
49 f.IntVar(&c.NumUnits, "n", 1, "number of service units to deploy")
50 f.IntVar(&c.NumUnits, "num-units", 1, "")
51 f.BoolVar(&c.Upgrade, "u", false, "increment local charm revision")
52 f.BoolVar(&c.Upgrade, "upgrade", false, "")
53 f.StringVar(&c.ConfPath, "config", "", "path to yaml-formatted service config")
54 f.StringVar(&c.RepoPath, "repository", os.Getenv("JUJU_REPOSITORY"), "local charm repository")
55 // TODO --constraints
56 if err := f.Parse(true, args); err != nil {
57 return err
58 }
59 args = f.Args()
60 switch len(args) {
61 case 2:
62 c.ServiceName = args[1]
63 fallthrough
64 case 1:
65 c.CharmName = args[0]
66 case 0:
67 return errors.New("no charm specified")
68 default:
69 return cmd.CheckEmpty(args[2:])
70 }
71 if c.NumUnits < 1 {
72 return errors.New("must deploy at least one unit")
73 }
74 return nil
75}
76
77func (c *DeployCommand) Run(ctx *cmd.Context) error {
78 panic("not implemented")
79}
080
=== added file 'cmd/juju/destroy.go.OTHER'
--- cmd/juju/destroy.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/juju/destroy.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,36 @@
1package main
2
3import (
4 "launchpad.net/gnuflag"
5 "launchpad.net/juju-core/juju/cmd"
6 "launchpad.net/juju-core/juju/juju"
7)
8
9// DestroyCommand destroys an environment.
10type DestroyCommand struct {
11 EnvName string
12}
13
14func (c *DestroyCommand) Info() *cmd.Info {
15 return &cmd.Info{
16 "destroy-environment", "[options]",
17 "terminate all machines and other associated resources for an environment",
18 "",
19 }
20}
21
22func (c *DestroyCommand) Init(f *gnuflag.FlagSet, args []string) error {
23 addEnvironFlags(&c.EnvName, f)
24 if err := f.Parse(true, args); err != nil {
25 return err
26 }
27 return cmd.CheckEmpty(f.Args())
28}
29
30func (c *DestroyCommand) Run(_ *cmd.Context) error {
31 conn, err := juju.NewConn(c.EnvName)
32 if err != nil {
33 return err
34 }
35 return conn.Destroy()
36}
037
=== added file 'cmd/juju/main.go.OTHER'
--- cmd/juju/main.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/juju/main.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,40 @@
1package main
2
3import (
4 "launchpad.net/gnuflag"
5 "launchpad.net/juju-core/juju/cmd"
6 "os"
7)
8
9// When we import an environment provider implementation
10// here, it will register itself with environs, and hence
11// be available to the juju command.
12import (
13 _ "launchpad.net/juju-core/juju/environs/ec2"
14)
15
16var jujuDoc = `
17juju provides easy, intelligent service orchestration on top of environments
18such as OpenStack, Amazon AWS, or bare metal.
19
20https://juju.ubuntu.com/
21`
22
23// Main registers subcommands for the juju executable, and hands over control
24// to the cmd package. This function is not redundant with main, because it
25// provides an entry point for testing with arbitrary command line arguments.
26func Main(args []string) {
27 juju := &cmd.SuperCommand{Name: "juju", Doc: jujuDoc, Log: &cmd.Log{}}
28 juju.Register(&BootstrapCommand{})
29 juju.Register(&DestroyCommand{})
30 os.Exit(cmd.Main(juju, cmd.DefaultContext(), args[1:]))
31}
32
33func main() {
34 Main(os.Args)
35}
36
37func addEnvironFlags(name *string, f *gnuflag.FlagSet) {
38 f.StringVar(name, "e", "", "juju environment to operate in")
39 f.StringVar(name, "environment", "", "")
40}
041
=== added directory 'cmd/jujuc'
=== added file 'cmd/jujuc/main.go.OTHER'
--- cmd/jujuc/main.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/jujuc/main.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,89 @@
1package main
2
3import (
4 "fmt"
5 "launchpad.net/juju-core/juju/cmd/jujuc/server"
6 "net/rpc"
7 "os"
8 "path/filepath"
9)
10
11var Help = `
12The jujuc command forwards invocations over RPC for execution by the juju
13unit agent. It expects to be called via a symlink named for the desired
14remote command, and expects JUJU_AGENT_SOCKET and JUJU_CONTEXT_ID be set
15in its environment.
16`[1:]
17
18func getenv(name string) (string, error) {
19 value := os.Getenv(name)
20 if value == "" {
21 return "", fmt.Errorf("%s not set", name)
22 }
23 return value, nil
24}
25
26func getwd() (string, error) {
27 dir, err := os.Getwd()
28 if err != nil {
29 return "", err
30 }
31 abs, err := filepath.Abs(dir)
32 if err != nil {
33 return "", err
34 }
35 return abs, nil
36}
37
38// Main uses JUJU_CONTEXT_ID and JUJU_AGENT_SOCKET to ask a running unit agent
39// to execute a Command on our behalf. Individual commands should be exposed
40// by symlinking the command name to this executable.
41// This function is not redundant with main, because it is exported, and can
42// thus be called by testing code.
43func Main(args []string) (code int, err error) {
44 commandName := filepath.Base(args[0])
45 if commandName == "jujuc" {
46 fmt.Fprint(os.Stderr, Help)
47 return 2, fmt.Errorf("jujuc should not be called directly")
48 }
49 code = 1
50 contextId, err := getenv("JUJU_CONTEXT_ID")
51 if err != nil {
52 return
53 }
54 dir, err := getwd()
55 if err != nil {
56 return
57 }
58 req := server.Request{
59 ContextId: contextId,
60 Dir: dir,
61 CommandName: commandName,
62 Args: args[1:],
63 }
64 socketPath, err := getenv("JUJU_AGENT_SOCKET")
65 if err != nil {
66 return
67 }
68 client, err := rpc.Dial("unix", socketPath)
69 if err != nil {
70 return
71 }
72 defer client.Close()
73 var resp server.Response
74 err = client.Call("Jujuc.Main", req, &resp)
75 if err != nil {
76 return
77 }
78 os.Stdout.Write(resp.Stdout)
79 os.Stderr.Write(resp.Stderr)
80 return resp.Code, nil
81}
82
83func main() {
84 code, err := Main(os.Args)
85 if err != nil {
86 fmt.Fprintf(os.Stderr, "error: %v\n", err)
87 }
88 os.Exit(code)
89}
090
=== added file 'cmd/jujuc/main_test.go.OTHER'
--- cmd/jujuc/main_test.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/jujuc/main_test.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,159 @@
1package main
2
3import (
4 "errors"
5 "flag"
6 "fmt"
7 "launchpad.net/gnuflag"
8 . "launchpad.net/gocheck"
9 "launchpad.net/juju-core/juju/cmd"
10 "launchpad.net/juju-core/juju/cmd/jujuc/server"
11 "os"
12 "os/exec"
13 "path/filepath"
14 "testing"
15)
16
17func Test(t *testing.T) { TestingT(t) }
18
19var flagRunMain = flag.Bool("run-main", false, "Run the application's main function for recursive testing")
20
21// Reentrancy point for testing (something as close as possible to) the jujuc
22// tool itself.
23func TestRunMain(t *testing.T) {
24 if *flagRunMain {
25 code, err := Main(flag.Args())
26 if err != nil {
27 fmt.Fprintf(os.Stderr, "error: %v\n", err)
28 }
29 os.Exit(code)
30 }
31}
32
33type RemoteCommand struct {
34 msg string
35}
36
37var expectUsage = `usage: remote [options]
38purpose: test jujuc
39
40options:
41--error (= "")
42 if set, fail
43
44here is some documentation
45`
46
47func (c *RemoteCommand) Info() *cmd.Info {
48 return &cmd.Info{
49 "remote", "", "test jujuc", "here is some documentation"}
50}
51
52func (c *RemoteCommand) Init(f *gnuflag.FlagSet, args []string) error {
53 f.StringVar(&c.msg, "error", "", "if set, fail")
54 if err := f.Parse(true, args); err != nil {
55 return err
56 }
57 return cmd.CheckEmpty(f.Args())
58}
59
60func (c *RemoteCommand) Run(ctx *cmd.Context) error {
61 if c.msg != "" {
62 return errors.New(c.msg)
63 }
64 fmt.Fprintf(ctx.Stdout, "success!\n")
65 return nil
66}
67
68func run(c *C, sockPath string, contextId string, exit int, cmd ...string) string {
69 args := append([]string{"-test.run", "TestRunMain", "-run-main", "--"}, cmd...)
70 ps := exec.Command(os.Args[0], args...)
71 ps.Dir = c.MkDir()
72 ps.Env = []string{
73 fmt.Sprintf("JUJU_AGENT_SOCKET=%s", sockPath),
74 fmt.Sprintf("JUJU_CONTEXT_ID=%s", contextId),
75 }
76 output, err := ps.CombinedOutput()
77 if exit == 0 {
78 c.Assert(err, IsNil)
79 } else {
80 c.Assert(err, ErrorMatches, fmt.Sprintf("exit status %d", exit))
81 }
82 return string(output)
83}
84
85type MainSuite struct {
86 sockPath string
87 server *server.Server
88}
89
90var _ = Suite(&MainSuite{})
91
92func (s *MainSuite) SetUpSuite(c *C) {
93 factory := func(contextId, cmdName string) (cmd.Command, error) {
94 if contextId != "bill" {
95 return nil, fmt.Errorf("bad context: %s", contextId)
96 }
97 if cmdName != "remote" {
98 return nil, fmt.Errorf("bad command: %s", cmdName)
99 }
100 return &RemoteCommand{}, nil
101 }
102 s.sockPath = filepath.Join(c.MkDir(), "test.sock")
103 srv, err := server.NewServer(factory, s.sockPath)
104 c.Assert(err, IsNil)
105 s.server = srv
106 go func() {
107 if err := s.server.Run(); err != nil {
108 c.Fatalf("server died: %s", err)
109 }
110 }()
111}
112
113func (s *MainSuite) TearDownSuite(c *C) {
114 s.server.Close()
115}
116
117var argsTests = []struct {
118 args []string
119 code int
120 output string
121}{
122 {[]string{"jujuc", "whatever"}, 2, Help + "error: jujuc should not be called directly\n"},
123 {[]string{"remote"}, 0, "success!\n"},
124 {[]string{"/path/to/remote"}, 0, "success!\n"},
125 {[]string{"unknown"}, 1, "error: bad request: bad command: unknown\n"},
126 {[]string{"remote", "--error", "borken"}, 1, "error: borken\n"},
127 {[]string{"remote", "--unknown"}, 2, expectUsage + "error: flag provided but not defined: --unknown\n"},
128 {[]string{"remote", "unwanted"}, 2, expectUsage + `error: unrecognized args: ["unwanted"]` + "\n"},
129}
130
131func (s *MainSuite) TestArgs(c *C) {
132 for _, t := range argsTests {
133 fmt.Println(t.args)
134 output := run(c, s.sockPath, "bill", t.code, t.args...)
135 c.Assert(output, Equals, t.output)
136 }
137}
138
139func (s *MainSuite) TestNoClientId(c *C) {
140 output := run(c, s.sockPath, "", 1, "remote")
141 c.Assert(output, Equals, "error: JUJU_CONTEXT_ID not set\n")
142}
143
144func (s *MainSuite) TestBadClientId(c *C) {
145 output := run(c, s.sockPath, "ben", 1, "remote")
146 c.Assert(output, Equals, "error: bad request: bad context: ben\n")
147}
148
149func (s *MainSuite) TestNoSockPath(c *C) {
150 output := run(c, "", "bill", 1, "remote")
151 c.Assert(output, Equals, "error: JUJU_AGENT_SOCKET not set\n")
152}
153
154func (s *MainSuite) TestBadSockPath(c *C) {
155 badSock := filepath.Join(c.MkDir(), "bad.sock")
156 output := run(c, badSock, "bill", 1, "remote")
157 err := fmt.Sprintf("error: dial unix %s: .*\n", badSock)
158 c.Assert(output, Matches, err)
159}
0160
=== added directory 'cmd/jujuc/server'
=== added file 'cmd/jujuc/server/config-get.go.OTHER'
--- cmd/jujuc/server/config-get.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/jujuc/server/config-get.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,66 @@
1package server
2
3import (
4 "launchpad.net/gnuflag"
5 "launchpad.net/juju-core/juju/cmd"
6)
7
8// ConfigGetCommand implements the config-get command.
9type ConfigGetCommand struct {
10 *ClientContext
11 Key string // The key to show. If empty, show all.
12 out output
13}
14
15func NewConfigGetCommand(ctx *ClientContext) (cmd.Command, error) {
16 if err := ctx.check(); err != nil {
17 return nil, err
18 }
19 return &ConfigGetCommand{ClientContext: ctx}, nil
20}
21
22func (c *ConfigGetCommand) Info() *cmd.Info {
23 return &cmd.Info{
24 "config-get", "[<key>]",
25 "print service configuration",
26 "If a key is given, only the value for that key will be printed.",
27 }
28}
29
30func (c *ConfigGetCommand) Init(f *gnuflag.FlagSet, args []string) error {
31 c.out.addFlags(f, "yaml", defaultFormatters)
32 if err := f.Parse(true, args); err != nil {
33 return err
34 }
35 args = f.Args()
36 if args == nil {
37 return nil
38 }
39 c.Key = args[0]
40 return cmd.CheckEmpty(args[1:])
41}
42
43func (c *ConfigGetCommand) Run(ctx *cmd.Context) error {
44 unit, err := c.State.Unit(c.LocalUnitName)
45 if err != nil {
46 return err
47 }
48 service, err := c.State.Service(unit.ServiceName())
49 if err != nil {
50 return err
51 }
52 conf, err := service.Config()
53 if err != nil {
54 return err
55 }
56 var value interface{}
57 if c.Key == "" {
58 value = conf.Map()
59 } else {
60 value, _ = conf.Get(c.Key)
61 }
62 if c.out.testMode {
63 return truthError(value)
64 }
65 return c.out.write(ctx, value)
66}
067
=== added file 'cmd/jujuc/server/config-get_test.go.OTHER'
--- cmd/jujuc/server/config-get_test.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/jujuc/server/config-get_test.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,125 @@
1package server_test
2
3import (
4 "io/ioutil"
5 . "launchpad.net/gocheck"
6 "launchpad.net/juju-core/juju/cmd"
7 "path/filepath"
8)
9
10type ConfigGetSuite struct {
11 UnitFixture
12}
13
14var _ = Suite(&ConfigGetSuite{})
15
16func (s *ConfigGetSuite) SetUpTest(c *C) {
17 s.UnitFixture.SetUpTest(c)
18 conf, err := s.service.Config()
19 c.Assert(err, IsNil)
20 conf.Update(map[string]interface{}{
21 "monsters": false,
22 "spline-reticulation": 45.0,
23 })
24 _, err = conf.Write()
25 c.Assert(err, IsNil)
26}
27
28var configGetYamlMap = "(spline-reticulation: 45\nmonsters: false\n|monsters: false\nspline-reticulation: 45\n)\n"
29var configGetTests = []struct {
30 args []string
31 out string
32}{
33 {[]string{"monsters"}, "false\n\n"},
34 {[]string{"--format", "yaml", "monsters"}, "false\n\n"},
35 {[]string{"--format", "json", "monsters"}, "false\n"},
36 {[]string{"spline-reticulation"}, "45\n\n"},
37 {[]string{"--format", "yaml", "spline-reticulation"}, "45\n\n"},
38 {[]string{"--format", "json", "spline-reticulation"}, "45\n"},
39 {[]string{"missing"}, ""},
40 {[]string{"--format", "yaml", "missing"}, ""},
41 {[]string{"--format", "json", "missing"}, "null\n"},
42 {nil, configGetYamlMap},
43 {[]string{"--format", "yaml"}, configGetYamlMap},
44 {[]string{"--format", "json"}, `{"monsters":false,"spline-reticulation":45}` + "\n"},
45}
46
47func (s *ConfigGetSuite) TestOutputFormat(c *C) {
48 for _, t := range configGetTests {
49 com, err := s.ctx.NewCommand("config-get")
50 c.Assert(err, IsNil)
51 ctx := dummyContext(c)
52 code := cmd.Main(com, ctx, t.args)
53 c.Assert(code, Equals, 0)
54 c.Assert(bufferString(ctx.Stderr), Equals, "")
55 c.Assert(bufferString(ctx.Stdout), Matches, t.out)
56 }
57}
58
59var configGetTestModeTests = []struct {
60 args []string
61 code int
62}{
63 {[]string{"monsters", "--test"}, 1},
64 {[]string{"spline-reticulation", "--test"}, 0},
65 {[]string{"missing", "--test"}, 1},
66 {[]string{"--test"}, 0},
67}
68
69func (s *ConfigGetSuite) TestTestMode(c *C) {
70 for _, t := range configGetTestModeTests {
71 com, err := s.ctx.NewCommand("config-get")
72 c.Assert(err, IsNil)
73 ctx := dummyContext(c)
74 code := cmd.Main(com, ctx, t.args)
75 c.Assert(code, Equals, t.code)
76 c.Assert(bufferString(ctx.Stderr), Equals, "")
77 c.Assert(bufferString(ctx.Stdout), Equals, "")
78 }
79}
80
81func (s *ConfigGetSuite) TestHelp(c *C) {
82 com, err := s.ctx.NewCommand("config-get")
83 c.Assert(err, IsNil)
84 ctx := dummyContext(c)
85 code := cmd.Main(com, ctx, []string{"--help"})
86 c.Assert(code, Equals, 0)
87 c.Assert(bufferString(ctx.Stdout), Equals, "")
88 c.Assert(bufferString(ctx.Stderr), Equals, `usage: config-get [options] [<key>]
89purpose: print service configuration
90
91options:
92--format (= yaml)
93 specify output format (json|yaml)
94-o, --output (= "")
95 specify an output file
96--test (= false)
97 returns non-zero exit code if value is false/zero/empty
98
99If a key is given, only the value for that key will be printed.
100`)
101}
102
103func (s *ConfigGetSuite) TestOutputPath(c *C) {
104 com, err := s.ctx.NewCommand("config-get")
105 c.Assert(err, IsNil)
106 ctx := dummyContext(c)
107 code := cmd.Main(com, ctx, []string{"--output", "some-file", "monsters"})
108 c.Assert(code, Equals, 0)
109 c.Assert(bufferString(ctx.Stderr), Equals, "")
110 c.Assert(bufferString(ctx.Stdout), Equals, "")
111 content, err := ioutil.ReadFile(filepath.Join(ctx.Dir, "some-file"))
112 c.Assert(err, IsNil)
113 c.Assert(string(content), Equals, "false\n\n")
114}
115
116func (s *ConfigGetSuite) TestUnknownArg(c *C) {
117 com, err := s.ctx.NewCommand("config-get")
118 c.Assert(err, IsNil)
119 err = com.Init(dummyFlagSet(), []string{"multiple", "keys"})
120 c.Assert(err, ErrorMatches, `unrecognized args: \["keys"\]`)
121}
122
123func (s *ConfigGetSuite) TestUnitCommand(c *C) {
124 s.AssertUnitCommand(c, "config-get")
125}
0126
=== added file 'cmd/jujuc/server/context.go.OTHER'
--- cmd/jujuc/server/context.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/jujuc/server/context.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,96 @@
1// The cmd/jujuc/server package implements the server side of the jujuc proxy
2// tool, which forwards command invocations to the unit agent process so that
3// they can be executed against specific state.
4package server
5
6import (
7 "fmt"
8 "launchpad.net/juju-core/juju/cmd"
9 "launchpad.net/juju-core/juju/state"
10 "os"
11 "os/exec"
12 "path/filepath"
13)
14
15// ClientContext is responsible for the state against which a jujuc-forwarded
16// command will execute; it implements the core of the various jujuc tools, and
17// is involved in constructing a suitable environment in which to execute a hook
18// (which is likely to call jujuc tools that need this specific ClientContext).
19type ClientContext struct {
20 Id string
21 State *state.State
22 LocalUnitName string
23 RemoteUnitName string
24 RelationName string
25}
26
27// checkUnitState returns an error if ctx has nil State or LocalUnitName fields.
28func (ctx *ClientContext) check() error {
29 if ctx.State == nil {
30 return fmt.Errorf("context %s cannot access state", ctx.Id)
31 }
32 if ctx.LocalUnitName == "" {
33 return fmt.Errorf("context %s is not attached to a unit", ctx.Id)
34 }
35 return nil
36}
37
38// newCommands maps Command names to initializers.
39var newCommands = map[string]func(*ClientContext) (cmd.Command, error){
40 "close-port": NewClosePortCommand,
41 "config-get": NewConfigGetCommand,
42 "juju-log": NewJujuLogCommand,
43 "open-port": NewOpenPortCommand,
44 "unit-get": NewUnitGetCommand,
45}
46
47// NewCommand returns an instance of the named Command, initialized to execute
48// against this ClientContext.
49func (ctx *ClientContext) NewCommand(name string) (cmd.Command, error) {
50 f := newCommands[name]
51 if f == nil {
52 return nil, fmt.Errorf("unknown command: %s", name)
53 }
54 return f(ctx)
55}
56
57// hookVars returns an os.Environ-style list of strings necessary to run a hook
58// such that it can know what environment it's operating in, and can call back
59// into ctx.
60func (ctx *ClientContext) hookVars(charmDir, socketPath string) []string {
61 vars := []string{
62 "APT_LISTCHANGES_FRONTEND=none",
63 "DEBIAN_FRONTEND=noninteractive",
64 "PATH=" + os.Getenv("PATH"),
65 "CHARM_DIR=" + charmDir,
66 "JUJU_CONTEXT_ID=" + ctx.Id,
67 "JUJU_AGENT_SOCKET=" + socketPath,
68 }
69 if ctx.LocalUnitName != "" {
70 vars = append(vars, "JUJU_UNIT_NAME="+ctx.LocalUnitName)
71 }
72 if ctx.RemoteUnitName != "" {
73 vars = append(vars, "JUJU_REMOTE_UNIT="+ctx.RemoteUnitName)
74 }
75 if ctx.RelationName != "" {
76 vars = append(vars, "JUJU_RELATION="+ctx.RelationName)
77 }
78 return vars
79}
80
81// RunHook executes a hook in an environment which allows it to to call back
82// into ctx to execute jujuc tools.
83func (ctx *ClientContext) RunHook(hookName, charmDir, socketPath string) error {
84 ps := exec.Command(filepath.Join(charmDir, "hooks", hookName))
85 ps.Env = ctx.hookVars(charmDir, socketPath)
86 ps.Dir = charmDir
87 if err := ps.Run(); err != nil {
88 if ee, ok := err.(*exec.Error); ok {
89 if os.IsNotExist(ee.Err) {
90 return nil
91 }
92 }
93 return err
94 }
95 return nil
96}
097
=== added file 'cmd/jujuc/server/context_test.go.OTHER'
--- cmd/jujuc/server/context_test.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/jujuc/server/context_test.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,145 @@
1package server_test
2
3import (
4 "fmt"
5 "io/ioutil"
6 . "launchpad.net/gocheck"
7 "launchpad.net/juju-core/juju/cmd/jujuc/server"
8 "launchpad.net/juju-core/juju/state"
9 "os"
10 "path/filepath"
11 "strings"
12)
13
14type GetCommandSuite struct{}
15
16var _ = Suite(&GetCommandSuite{})
17
18var getCommandTests = []struct {
19 name string
20 err string
21}{
22 {"close-port", ""},
23 {"config-get", ""},
24 {"juju-log", ""},
25 {"open-port", ""},
26 {"unit-get", ""},
27 {"random", "unknown command: random"},
28}
29
30func (s *GetCommandSuite) TestGetCommand(c *C) {
31 ctx := &server.ClientContext{
32 Id: "ctxid",
33 State: &state.State{},
34 LocalUnitName: "minecraft/0",
35 }
36 for _, t := range getCommandTests {
37 com, err := ctx.NewCommand(t.name)
38 if t.err == "" {
39 // At this level, just check basic sanity; commands are tested in
40 // more detail elsewhere.
41 c.Assert(err, IsNil)
42 c.Assert(com.Info().Name, Equals, t.name)
43 } else {
44 c.Assert(com, IsNil)
45 c.Assert(err, ErrorMatches, t.err)
46 }
47 }
48}
49
50type RunHookSuite struct {
51 outPath string
52}
53
54var _ = Suite(&RunHookSuite{})
55
56// makeCharm constructs a fake charm dir containing a single named hook with
57// permissions perm and exit code code. It returns the charm directory and the
58// path to which the hook script will write environment variables.
59func makeCharm(c *C, hookName string, perm os.FileMode, code int) (charmDir, outPath string) {
60 charmDir = c.MkDir()
61 hooksDir := filepath.Join(charmDir, "hooks")
62 err := os.Mkdir(hooksDir, 0755)
63 c.Assert(err, IsNil)
64 hook, err := os.OpenFile(filepath.Join(hooksDir, hookName), os.O_CREATE|os.O_WRONLY, perm)
65 c.Assert(err, IsNil)
66 defer hook.Close()
67 outPath = filepath.Join(c.MkDir(), "hook.out")
68 _, err = fmt.Fprintf(hook, "#!/bin/bash\nenv > %s\nexit %d", outPath, code)
69 c.Assert(err, IsNil)
70 return charmDir, outPath
71}
72
73func AssertEnvContains(c *C, lines []string, env map[string]string) {
74 for k, v := range env {
75 sought := k + "=" + v
76 found := false
77 for _, line := range lines {
78 if line == sought {
79 found = true
80 continue
81 }
82 }
83 comment := Commentf("expected to find %v among %v", sought, lines)
84 c.Assert(found, Equals, true, comment)
85 }
86}
87
88func AssertEnv(c *C, outPath string, env map[string]string) {
89 out, err := ioutil.ReadFile(outPath)
90 c.Assert(err, IsNil)
91 lines := strings.Split(string(out), "\n")
92 AssertEnvContains(c, lines, env)
93 AssertEnvContains(c, lines, map[string]string{
94 "PATH": os.Getenv("PATH"),
95 "DEBIAN_FRONTEND": "noninteractive",
96 "APT_LISTCHANGES_FRONTEND": "none",
97 })
98}
99
100func (s *RunHookSuite) TestNoHook(c *C) {
101 ctx := &server.ClientContext{}
102 err := ctx.RunHook("tree-fell-in-forest", c.MkDir(), "")
103 c.Assert(err, IsNil)
104}
105
106func (s *RunHookSuite) TestNonExecutableHook(c *C) {
107 ctx := &server.ClientContext{}
108 charmDir, _ := makeCharm(c, "something-happened", 0600, 0)
109 err := ctx.RunHook("something-happened", charmDir, "")
110 c.Assert(err, ErrorMatches, `exec: ".*/something-happened": permission denied`)
111}
112
113func (s *RunHookSuite) TestBadHook(c *C) {
114 ctx := &server.ClientContext{Id: "ctx-id"}
115 charmDir, outPath := makeCharm(c, "occurrence-occurred", 0700, 99)
116 socketPath := "/path/to/socket"
117 err := ctx.RunHook("occurrence-occurred", charmDir, socketPath)
118 c.Assert(err, ErrorMatches, "exit status 99")
119 AssertEnv(c, outPath, map[string]string{
120 "CHARM_DIR": charmDir,
121 "JUJU_AGENT_SOCKET": socketPath,
122 "JUJU_CONTEXT_ID": "ctx-id",
123 })
124}
125
126func (s *RunHookSuite) TestGoodHookWithVars(c *C) {
127 ctx := &server.ClientContext{
128 Id: "some-id",
129 LocalUnitName: "local/99",
130 RemoteUnitName: "remote/123",
131 RelationName: "rel",
132 }
133 charmDir, outPath := makeCharm(c, "something-happened", 0700, 0)
134 socketPath := "/path/to/socket"
135 err := ctx.RunHook("something-happened", charmDir, socketPath)
136 c.Assert(err, IsNil)
137 AssertEnv(c, outPath, map[string]string{
138 "CHARM_DIR": charmDir,
139 "JUJU_AGENT_SOCKET": socketPath,
140 "JUJU_CONTEXT_ID": "some-id",
141 "JUJU_UNIT_NAME": "local/99",
142 "JUJU_REMOTE_UNIT": "remote/123",
143 "JUJU_RELATION": "rel",
144 })
145}
0146
=== added file 'cmd/jujuc/server/juju-log.go.OTHER'
--- cmd/jujuc/server/juju-log.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/jujuc/server/juju-log.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,57 @@
1package server
2
3import (
4 "errors"
5 "launchpad.net/gnuflag"
6 "launchpad.net/juju-core/juju/cmd"
7 "launchpad.net/juju-core/juju/log"
8 "strings"
9)
10
11// JujuLogCommand implements the juju-log command.
12type JujuLogCommand struct {
13 *ClientContext
14 Message string
15 Debug bool
16}
17
18func NewJujuLogCommand(ctx *ClientContext) (cmd.Command, error) {
19 return &JujuLogCommand{ClientContext: ctx}, nil
20}
21
22func (c *JujuLogCommand) Info() *cmd.Info {
23 return &cmd.Info{"juju-log", "<message>", "write a message to the juju log", ""}
24}
25
26func (c *JujuLogCommand) Init(f *gnuflag.FlagSet, args []string) error {
27 f.BoolVar(&c.Debug, "debug", false, "log at debug level")
28 if err := f.Parse(true, args); err != nil {
29 return err
30 }
31 args = f.Args()
32 if args == nil {
33 return errors.New("no message specified")
34 }
35 c.Message = strings.Join(args, " ")
36 return nil
37}
38
39func (c *JujuLogCommand) Run(_ *cmd.Context) error {
40 s := []string{}
41 if c.LocalUnitName != "" {
42 s = append(s, c.LocalUnitName)
43 }
44 if c.RelationName != "" {
45 s = append(s, c.RelationName)
46 }
47 msg := c.Message
48 if len(s) > 0 {
49 msg = strings.Join(s, " ") + ": " + msg
50 }
51 if c.Debug {
52 log.Debugf("%s", msg)
53 } else {
54 log.Printf("%s", msg)
55 }
56 return nil
57}
058
=== added file 'cmd/jujuc/server/juju-log_test.go.OTHER'
--- cmd/jujuc/server/juju-log_test.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/jujuc/server/juju-log_test.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,82 @@
1package server_test
2
3import (
4 "bytes"
5 "fmt"
6 "launchpad.net/gnuflag"
7 . "launchpad.net/gocheck"
8 "launchpad.net/juju-core/juju/cmd"
9 "launchpad.net/juju-core/juju/cmd/jujuc/server"
10 "launchpad.net/juju-core/juju/log"
11 stdlog "log"
12)
13
14type JujuLogSuite struct{}
15
16var _ = Suite(&JujuLogSuite{})
17
18func pushLog(debug bool) (buf *bytes.Buffer, pop func()) {
19 oldTarget, oldDebug := log.Target, log.Debug
20 buf = new(bytes.Buffer)
21 log.Target, log.Debug = stdlog.New(buf, "", 0), debug
22 return buf, func() {
23 log.Target, log.Debug = oldTarget, oldDebug
24 }
25}
26
27func dummyFlagSet() *gnuflag.FlagSet {
28 return gnuflag.NewFlagSet("", gnuflag.ContinueOnError)
29}
30
31var commonLogTests = []struct {
32 debugEnabled bool
33 debugFlag bool
34 target string
35}{
36 {false, false, "JUJU"},
37 {false, true, ""},
38 {true, false, "JUJU"},
39 {true, true, "JUJU:DEBUG"},
40}
41
42func assertLogs(c *C, ctx *server.ClientContext, badge string) {
43 msg1 := "the chickens"
44 msg2 := "are 110% AWESOME"
45 com, err := ctx.NewCommand("juju-log")
46 c.Assert(err, IsNil)
47 for _, t := range commonLogTests {
48 buf, pop := pushLog(t.debugEnabled)
49 defer pop()
50
51 var args []string
52 if t.debugFlag {
53 args = []string{"--debug", msg1, msg2}
54 } else {
55 args = []string{msg1, msg2}
56 }
57 code := cmd.Main(com, &cmd.Context{}, args)
58 c.Assert(code, Equals, 0)
59
60 if t.target == "" {
61 c.Assert(buf.String(), Equals, "")
62 } else {
63 expect := fmt.Sprintf("%s %s: %s %s\n", t.target, badge, msg1, msg2)
64 c.Assert(buf.String(), Equals, expect)
65 }
66 }
67}
68
69func (s *JujuLogSuite) TestBadges(c *C) {
70 local := &server.ClientContext{LocalUnitName: "minecraft/0"}
71 assertLogs(c, local, "minecraft/0")
72 relation := &server.ClientContext{LocalUnitName: "minecraft/0", RelationName: "bot"}
73 assertLogs(c, relation, "minecraft/0 bot")
74}
75
76func (s *JujuLogSuite) TestRequiresMessage(c *C) {
77 ctx := &server.ClientContext{}
78 com, err := ctx.NewCommand("juju-log")
79 c.Assert(err, IsNil)
80 err = com.Init(dummyFlagSet(), nil)
81 c.Assert(err, ErrorMatches, "no message specified")
82}
083
=== added file 'cmd/jujuc/server/output.go.OTHER'
--- cmd/jujuc/server/output.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/jujuc/server/output.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,149 @@
1package server
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "launchpad.net/gnuflag"
8 "launchpad.net/goyaml"
9 "launchpad.net/juju-core/juju/cmd"
10 "os"
11 "reflect"
12 "sort"
13 "strings"
14)
15
16// formatter converts an arbitrary object into a []byte.
17type formatter func(value interface{}) ([]byte, error)
18
19// formatYaml marshals value to a yaml-formatted []byte, unless value is nil.
20func formatYaml(value interface{}) ([]byte, error) {
21 if value == nil {
22 return nil, nil
23 }
24 return goyaml.Marshal(value)
25}
26
27// defaultFormatters are used by many jujuc Commands.
28var defaultFormatters = map[string]formatter{
29 "yaml": formatYaml,
30 "json": json.Marshal,
31}
32
33// formatterValue implements gnuflag.Value for the --format flag.
34type formatterValue struct {
35 name string
36 formatters map[string]formatter
37}
38
39// newFormatterValue returns a new formatterValue. The initial formatter name
40// must be present in formatters.
41func newFormatterValue(initial string, formatters map[string]formatter) *formatterValue {
42 v := &formatterValue{formatters: formatters}
43 if err := v.Set(initial); err != nil {
44 panic(err)
45 }
46 return v
47}
48
49// Set stores the chosen formatter name in v.name.
50func (v *formatterValue) Set(value string) error {
51 if v.formatters[value] == nil {
52 return fmt.Errorf("unknown format: %s", value)
53 }
54 v.name = value
55 return nil
56}
57
58// String returns the chosen formatter name.
59func (v *formatterValue) String() string {
60 return v.name
61}
62
63// doc returns documentation for the --format flag.
64func (v *formatterValue) doc() string {
65 choices := make([]string, len(v.formatters))
66 i := 0
67 for name := range v.formatters {
68 choices[i] = name
69 i++
70 }
71 sort.Strings(choices)
72 return "specify output format (" + strings.Join(choices, "|") + ")"
73}
74
75// format runs the chosen formatter on value.
76func (v *formatterValue) format(value interface{}) ([]byte, error) {
77 return v.formatters[v.name](value)
78}
79
80// output is responsible for interpreting output-related command line flags
81// and writing a value to a file or to stdout as directed. The testMode field,
82// controlled by the --test flag, is used to indicate that output should be
83// suppressed and communicated entirely in the process exit code.
84type output struct {
85 formatter *formatterValue
86 outPath string
87 testMode bool
88}
89
90// addFlags injects appropriate command line flags into f.
91func (c *output) addFlags(f *gnuflag.FlagSet, name string, formatters map[string]formatter) {
92 c.formatter = newFormatterValue(name, formatters)
93 f.Var(c.formatter, "format", c.formatter.doc())
94 f.StringVar(&c.outPath, "o", "", "specify an output file")
95 f.StringVar(&c.outPath, "output", "", "")
96 f.BoolVar(&c.testMode, "test", false, "returns non-zero exit code if value is false/zero/empty")
97}
98
99// write formats and outputs value as directed by the --format and --output
100// command line flags.
101func (c *output) write(ctx *cmd.Context, value interface{}) (err error) {
102 var target io.Writer
103 if c.outPath == "" {
104 target = ctx.Stdout
105 } else {
106 path := ctx.AbsPath(c.outPath)
107 if target, err = os.Create(path); err != nil {
108 return
109 }
110 }
111 bytes, err := c.formatter.format(value)
112 if err != nil {
113 return
114 }
115 if bytes != nil {
116 _, err = target.Write(bytes)
117 if err == nil {
118 _, err = target.Write([]byte{'\n'})
119 }
120 }
121 return
122}
123
124// truthError returns cmd.ErrSilent if value is nil, false, or 0, or an empty
125// array, map, slice, or string.
126func truthError(value interface{}) error {
127 b := true
128 v := reflect.ValueOf(value)
129 switch v.Kind() {
130 case reflect.Invalid:
131 b = false
132 case reflect.Bool:
133 b = v.Bool()
134 case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
135 b = v.Len() != 0
136 case reflect.Float32, reflect.Float64:
137 b = v.Float() != 0
138 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
139 b = v.Int() != 0
140 case reflect.Uint, reflect.Uintptr, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
141 b = v.Uint() != 0
142 case reflect.Interface, reflect.Ptr:
143 b = !v.IsNil()
144 }
145 if b {
146 return nil
147 }
148 return cmd.ErrSilent
149}
0150
=== added file 'cmd/jujuc/server/ports.go.OTHER'
--- cmd/jujuc/server/ports.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/jujuc/server/ports.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,100 @@
1package server
2
3import (
4 "errors"
5 "fmt"
6 "launchpad.net/gnuflag"
7 "launchpad.net/juju-core/juju/cmd"
8 "launchpad.net/juju-core/juju/state"
9 "strconv"
10 "strings"
11)
12
13const portFormat = "<port>[/<protocol>]"
14
15// portCommand implements the open-port and close-port commands.
16type portCommand struct {
17 *ClientContext
18 info *cmd.Info
19 action func(*state.Unit, string, int) error
20 Protocol string
21 Port int
22}
23
24func (c *portCommand) Info() *cmd.Info {
25 return c.info
26}
27
28func badPort(value interface{}) error {
29 return fmt.Errorf(`port must be in the range [1, 65535]; got "%v"`, value)
30}
31
32func (c *portCommand) Init(f *gnuflag.FlagSet, args []string) error {
33 if err := f.Parse(true, args); err != nil {
34 return err
35 }
36 args = f.Args()
37 if args == nil {
38 return errors.New("no port specified")
39 }
40 parts := strings.Split(args[0], "/")
41 if len(parts) > 2 {
42 return fmt.Errorf("expected %s; got %q", portFormat, args[0])
43 }
44 port, err := strconv.Atoi(parts[0])
45 if err != nil {
46 return badPort(parts[0])
47 }
48 if port < 1 || port > 65535 {
49 return badPort(port)
50 }
51 protocol := "tcp"
52 if len(parts) == 2 {
53 protocol = strings.ToLower(parts[1])
54 if protocol != "tcp" && protocol != "udp" {
55 return fmt.Errorf(`protocol must be "tcp" or "udp"; got %q`, protocol)
56 }
57 }
58 c.Port = port
59 c.Protocol = protocol
60 return cmd.CheckEmpty(args[1:])
61}
62
63func (c *portCommand) Run(_ *cmd.Context) error {
64 unit, err := c.State.Unit(c.LocalUnitName)
65 if err != nil {
66 return err
67 }
68 return c.action(unit, c.Protocol, c.Port)
69}
70
71var openPortInfo = &cmd.Info{
72 "open-port", portFormat, "register a port to open",
73 "The port will only be open while the service is exposed.",
74}
75
76func NewOpenPortCommand(ctx *ClientContext) (cmd.Command, error) {
77 if err := ctx.check(); err != nil {
78 return nil, err
79 }
80 return &portCommand{
81 ClientContext: ctx,
82 info: openPortInfo,
83 action: (*state.Unit).OpenPort,
84 }, nil
85}
86
87var closePortInfo = &cmd.Info{
88 "close-port", portFormat, "ensure a port is always closed", "",
89}
90
91func NewClosePortCommand(ctx *ClientContext) (cmd.Command, error) {
92 if err := ctx.check(); err != nil {
93 return nil, err
94 }
95 return &portCommand{
96 ClientContext: ctx,
97 info: closePortInfo,
98 action: (*state.Unit).ClosePort,
99 }, nil
100}
0101
=== added file 'cmd/jujuc/server/ports_test.go.OTHER'
--- cmd/jujuc/server/ports_test.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/jujuc/server/ports_test.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,86 @@
1package server_test
2
3import (
4 . "launchpad.net/gocheck"
5 "launchpad.net/juju-core/juju/cmd"
6 "launchpad.net/juju-core/juju/state"
7)
8
9type PortsSuite struct {
10 UnitFixture
11}
12
13var _ = Suite(&PortsSuite{})
14
15var portsTests = []struct {
16 cmd []string
17 open []state.Port
18}{
19 {[]string{"open-port", "80"}, []state.Port{{"tcp", 80}}},
20 {[]string{"open-port", "99/tcp"}, []state.Port{{"tcp", 80}, {"tcp", 99}}},
21 {[]string{"close-port", "80/TCP"}, []state.Port{{"tcp", 99}}},
22 {[]string{"open-port", "123/udp"}, []state.Port{{"tcp", 99}, {"udp", 123}}},
23 {[]string{"close-port", "9999/UDP"}, []state.Port{{"tcp", 99}, {"udp", 123}}},
24}
25
26func (s *PortsSuite) TestOpenClose(c *C) {
27 for _, t := range portsTests {
28 com, err := s.ctx.NewCommand(t.cmd[0])
29 c.Assert(err, IsNil)
30 ctx := dummyContext(c)
31 code := cmd.Main(com, ctx, t.cmd[1:])
32 c.Assert(code, Equals, 0)
33 c.Assert(bufferString(ctx.Stdout), Equals, "")
34 c.Assert(bufferString(ctx.Stderr), Equals, "")
35 open, err := s.unit.OpenPorts()
36 c.Assert(err, IsNil)
37 c.Assert(open, DeepEquals, t.open)
38 }
39}
40
41var badPortsTests = []struct {
42 args []string
43 err string
44}{
45 {nil, "no port specified"},
46 {[]string{"0"}, `port must be in the range \[1, 65535\]; got "0"`},
47 {[]string{"65536"}, `port must be in the range \[1, 65535\]; got "65536"`},
48 {[]string{"two"}, `port must be in the range \[1, 65535\]; got "two"`},
49 {[]string{"80/http"}, `protocol must be "tcp" or "udp"; got "http"`},
50 {[]string{"blah/blah/blah"}, `expected <port>\[/<protocol>\]; got "blah/blah/blah"`},
51 {[]string{"123", "haha"}, `unrecognized args: \["haha"\]`},
52}
53
54func (s *PortsSuite) TestBadArgs(c *C) {
55 for _, name := range []string{"open-port", "close-port"} {
56 for _, t := range badPortsTests {
57 com, err := s.ctx.NewCommand(name)
58 c.Assert(err, IsNil)
59 err = com.Init(dummyFlagSet(), t.args)
60 c.Assert(err, ErrorMatches, t.err)
61 }
62 }
63}
64
65func (s *PortsSuite) TestHelp(c *C) {
66 open, err := s.ctx.NewCommand("open-port")
67 c.Assert(err, IsNil)
68 c.Assert(string(open.Info().Help(dummyFlagSet())), Equals, `
69usage: open-port <port>[/<protocol>]
70purpose: register a port to open
71
72The port will only be open while the service is exposed.
73`[1:])
74
75 close, err := s.ctx.NewCommand("close-port")
76 c.Assert(err, IsNil)
77 c.Assert(string(close.Info().Help(dummyFlagSet())), Equals, `
78usage: close-port <port>[/<protocol>]
79purpose: ensure a port is always closed
80`[1:])
81}
82
83func (s *PortsSuite) TestUnitCommands(c *C) {
84 s.AssertUnitCommand(c, "open-port")
85 s.AssertUnitCommand(c, "close-port")
86}
087
=== added file 'cmd/jujuc/server/server.go.OTHER'
--- cmd/jujuc/server/server.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/jujuc/server/server.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,133 @@
1// The cmd/jujuc/server package allows a process to expose an RPC interface that
2// allows client processes to delegate execution of cmd.Commands to a server
3// process (with the exposed commands amenable to specialisation by context id).
4package server
5
6import (
7 "bytes"
8 "fmt"
9 "launchpad.net/juju-core/juju/cmd"
10 "net"
11 "net/rpc"
12 "os"
13 "path/filepath"
14 "sync"
15)
16
17// Request contains the information necessary to run a Command remotely.
18type Request struct {
19 ContextId string
20 Dir string
21 CommandName string
22 Args []string
23}
24
25// Response contains the return code and output generated by a Request.
26type Response struct {
27 Code int
28 Stdout []byte
29 Stderr []byte
30}
31
32// CmdGetter looks up a Command implementation connected to a particular Context.
33type CmdGetter func(contextId, cmdName string) (cmd.Command, error)
34
35// Jujuc implements the jujuc command in the form required by net/rpc.
36type Jujuc struct {
37 getCmd CmdGetter
38}
39
40// badReqErr returns an error indicating a bad Request.
41func badReqErr(format string, v ...interface{}) error {
42 return fmt.Errorf("bad request: "+format, v...)
43}
44
45// Main runs the Command specified by req, and fills in resp.
46func (j *Jujuc) Main(req Request, resp *Response) error {
47 if req.CommandName == "" {
48 return badReqErr("command not specified")
49 }
50 if !filepath.IsAbs(req.Dir) {
51 return badReqErr("Dir is not absolute")
52 }
53 c, err := j.getCmd(req.ContextId, req.CommandName)
54 if err != nil {
55 return badReqErr("%s", err)
56 }
57 var stdout, stderr bytes.Buffer
58 ctx := &cmd.Context{req.Dir, &stdout, &stderr}
59 resp.Code = cmd.Main(c, ctx, req.Args)
60 resp.Stdout = stdout.Bytes()
61 resp.Stderr = stderr.Bytes()
62 return nil
63}
64
65// Server implements a server that serves command invocations via
66// a unix domain socket.
67type Server struct {
68 socketPath string
69 listener net.Listener
70 server *rpc.Server
71 closed chan bool
72 closing chan bool
73 wg sync.WaitGroup
74}
75
76// NewServer creates an RPC server bound to socketPath, which can execute
77// remote command invocations against an appropriate Context. It will not
78// actually do so until Run is called.
79func NewServer(getCmd CmdGetter, socketPath string) (*Server, error) {
80 server := rpc.NewServer()
81 if err := server.Register(&Jujuc{getCmd}); err != nil {
82 return nil, err
83 }
84 listener, err := net.Listen("unix", socketPath)
85 if err != nil {
86 return nil, err
87 }
88 s := &Server{
89 socketPath: socketPath,
90 listener: listener,
91 server: server,
92 closed: make(chan bool),
93 closing: make(chan bool),
94 }
95 return s, nil
96}
97
98// Run accepts new connections until it encounters an error, or until Close is
99// called, and then blocks until all existing connections have been closed.
100func (s *Server) Run() (err error) {
101 var conn net.Conn
102 for {
103 conn, err = s.listener.Accept()
104 if err != nil {
105 break
106 }
107 s.wg.Add(1)
108 go func(conn net.Conn) {
109 s.server.ServeConn(conn)
110 s.wg.Done()
111 }(conn)
112 }
113 select {
114 case <-s.closing:
115 // Someone has called Close(), so it is overwhelmingly likely that
116 // the error from Accept is a direct result of the Listener being
117 // closed, and can therefore be safely ignored.
118 err = nil
119 default:
120 }
121 s.wg.Wait()
122 close(s.closed)
123 return
124}
125
126// Close immediately stops accepting connections, and blocks until all existing
127// connections have been closed.
128func (s *Server) Close() {
129 close(s.closing)
130 s.listener.Close()
131 os.Remove(s.socketPath)
132 <-s.closed
133}
0134
=== added file 'cmd/jujuc/server/server_test.go.OTHER'
--- cmd/jujuc/server/server_test.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/jujuc/server/server_test.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,146 @@
1package server_test
2
3import (
4 "errors"
5 "fmt"
6 "io/ioutil"
7 "launchpad.net/gnuflag"
8 . "launchpad.net/gocheck"
9 "launchpad.net/juju-core/juju/cmd"
10 "launchpad.net/juju-core/juju/cmd/jujuc/server"
11 "net/rpc"
12 "os"
13 "path/filepath"
14)
15
16type RpcCommand struct {
17 Value string
18}
19
20func (c *RpcCommand) Info() *cmd.Info {
21 return &cmd.Info{"remote", "", "act at a distance", "blah doc"}
22}
23
24func (c *RpcCommand) Init(f *gnuflag.FlagSet, args []string) error {
25 f.StringVar(&c.Value, "value", "", "doc")
26 if err := f.Parse(true, args); err != nil {
27 return err
28 }
29 return cmd.CheckEmpty(f.Args())
30}
31
32func (c *RpcCommand) Run(ctx *cmd.Context) error {
33 if c.Value == "error" {
34 return errors.New("blam")
35 }
36 ctx.Stdout.Write([]byte("eye of newt\n"))
37 ctx.Stderr.Write([]byte("toe of frog\n"))
38 return ioutil.WriteFile(ctx.AbsPath("local"), []byte(c.Value), 0644)
39}
40
41func factory(contextId, cmdName string) (cmd.Command, error) {
42 if contextId != "validCtx" {
43 return nil, fmt.Errorf("unknown context %q", contextId)
44 }
45 if cmdName != "remote" {
46 return nil, fmt.Errorf("unknown command %q", cmdName)
47 }
48 return &RpcCommand{}, nil
49}
50
51type ServerSuite struct {
52 server *server.Server
53 sockPath string
54 err chan error
55}
56
57var _ = Suite(&ServerSuite{})
58
59func (s *ServerSuite) SetUpTest(c *C) {
60 s.sockPath = filepath.Join(c.MkDir(), "test.sock")
61 srv, err := server.NewServer(factory, s.sockPath)
62 c.Assert(err, IsNil)
63 c.Assert(srv, NotNil)
64 s.server = srv
65 s.err = make(chan error)
66 go func() { s.err <- s.server.Run() }()
67}
68
69func (s *ServerSuite) TearDownTest(c *C) {
70 s.server.Close()
71 c.Assert(<-s.err, IsNil)
72 _, err := os.Open(s.sockPath)
73 c.Assert(os.IsNotExist(err), Equals, true)
74}
75
76func (s *ServerSuite) Call(c *C, req server.Request) (resp server.Response, err error) {
77 client, err := rpc.Dial("unix", s.sockPath)
78 c.Assert(err, IsNil)
79 defer client.Close()
80 err = client.Call("Jujuc.Main", req, &resp)
81 return resp, err
82}
83
84func (s *ServerSuite) TestHappyPath(c *C) {
85 dir := c.MkDir()
86 resp, err := s.Call(c, server.Request{
87 "validCtx", dir, "remote", []string{"--value", "something"}})
88 c.Assert(err, IsNil)
89 c.Assert(resp.Code, Equals, 0)
90 c.Assert(string(resp.Stdout), Equals, "eye of newt\n")
91 c.Assert(string(resp.Stderr), Equals, "toe of frog\n")
92 content, err := ioutil.ReadFile(filepath.Join(dir, "local"))
93 c.Assert(err, IsNil)
94 c.Assert(string(content), Equals, "something")
95}
96
97func (s *ServerSuite) TestBadCommandName(c *C) {
98 dir := c.MkDir()
99 _, err := s.Call(c, server.Request{"validCtx", dir, "", nil})
100 c.Assert(err, ErrorMatches, "bad request: command not specified")
101 _, err = s.Call(c, server.Request{"validCtx", dir, "witchcraft", nil})
102 c.Assert(err, ErrorMatches, `bad request: unknown command "witchcraft"`)
103}
104
105func (s *ServerSuite) TestBadDir(c *C) {
106 for _, req := range []server.Request{
107 {"validCtx", "", "anything", nil},
108 {"validCtx", "foo/bar", "anything", nil},
109 } {
110 _, err := s.Call(c, req)
111 c.Assert(err, ErrorMatches, "bad request: Dir is not absolute")
112 }
113}
114
115func (s *ServerSuite) TestBadContextId(c *C) {
116 _, err := s.Call(c, server.Request{"whatever", c.MkDir(), "remote", nil})
117 c.Assert(err, ErrorMatches, `bad request: unknown context "whatever"`)
118}
119
120func (s *ServerSuite) AssertBadCommand(c *C, args []string, code int) server.Response {
121 resp, err := s.Call(c, server.Request{"validCtx", c.MkDir(), args[0], args[1:]})
122 c.Assert(err, IsNil)
123 c.Assert(resp.Code, Equals, code)
124 return resp
125}
126
127func (s *ServerSuite) TestParseError(c *C) {
128 resp := s.AssertBadCommand(c, []string{"remote", "--cheese"}, 2)
129 c.Assert(string(resp.Stdout), Equals, "")
130 c.Assert(string(resp.Stderr), Equals, `usage: remote [options]
131purpose: act at a distance
132
133options:
134--value (= "")
135 doc
136
137blah doc
138error: flag provided but not defined: --cheese
139`)
140}
141
142func (s *ServerSuite) TestBrokenCommand(c *C) {
143 resp := s.AssertBadCommand(c, []string{"remote", "--value", "error"}, 1)
144 c.Assert(string(resp.Stdout), Equals, "")
145 c.Assert(string(resp.Stderr), Equals, "error: blam\n")
146}
0147
=== added file 'cmd/jujuc/server/unit-get.go.OTHER'
--- cmd/jujuc/server/unit-get.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/jujuc/server/unit-get.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,66 @@
1package server
2
3import (
4 "errors"
5 "fmt"
6 "launchpad.net/gnuflag"
7 "launchpad.net/juju-core/juju/cmd"
8 "launchpad.net/juju-core/juju/state"
9)
10
11// UnitGetCommand implements the unit-get command.
12type UnitGetCommand struct {
13 *ClientContext
14 Key string
15 out output
16}
17
18func NewUnitGetCommand(ctx *ClientContext) (cmd.Command, error) {
19 if err := ctx.check(); err != nil {
20 return nil, err
21 }
22 return &UnitGetCommand{ClientContext: ctx}, nil
23}
24
25func (c *UnitGetCommand) Info() *cmd.Info {
26 return &cmd.Info{
27 "unit-get", "<setting>", "print public-address or private-address", "",
28 }
29}
30
31func (c *UnitGetCommand) Init(f *gnuflag.FlagSet, args []string) error {
32 c.out.addFlags(f, "yaml", defaultFormatters)
33 if err := f.Parse(true, args); err != nil {
34 return err
35 }
36 args = f.Args()
37 if args == nil {
38 return errors.New("no setting specified")
39 }
40 if args[0] != "private-address" && args[0] != "public-address" {
41 return fmt.Errorf("unknown setting %q", args[0])
42 }
43 c.Key = args[0]
44 return cmd.CheckEmpty(args[1:])
45}
46
47func (c *UnitGetCommand) Run(ctx *cmd.Context) (err error) {
48 var unit *state.Unit
49 unit, err = c.State.Unit(c.LocalUnitName)
50 if err != nil {
51 return
52 }
53 var value string
54 if c.Key == "private-address" {
55 value, err = unit.PrivateAddress()
56 } else {
57 value, err = unit.PublicAddress()
58 }
59 if err != nil {
60 return
61 }
62 if c.out.testMode {
63 return truthError(value)
64 }
65 return c.out.write(ctx, value)
66}
067
=== added file 'cmd/jujuc/server/unit-get_test.go.OTHER'
--- cmd/jujuc/server/unit-get_test.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/jujuc/server/unit-get_test.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,109 @@
1package server_test
2
3import (
4 "io/ioutil"
5 . "launchpad.net/gocheck"
6 "launchpad.net/juju-core/juju/cmd"
7 "path/filepath"
8)
9
10type UnitGetSuite struct {
11 UnitFixture
12}
13
14var _ = Suite(&UnitGetSuite{})
15
16func (s *UnitGetSuite) SetUpTest(c *C) {
17 s.UnitFixture.SetUpTest(c)
18 err := s.unit.SetPublicAddress("gimli.minecraft.example.com")
19 c.Assert(err, IsNil)
20 err = s.unit.SetPrivateAddress("192.168.0.99")
21 c.Assert(err, IsNil)
22}
23
24var unitGetTests = []struct {
25 args []string
26 out string
27}{
28 {[]string{"private-address"}, "192.168.0.99\n\n"},
29 {[]string{"private-address", "--format", "yaml"}, "192.168.0.99\n\n"},
30 {[]string{"private-address", "--format", "json"}, `"192.168.0.99"` + "\n"},
31 {[]string{"public-address"}, "gimli.minecraft.example.com\n\n"},
32 {[]string{"public-address", "--format", "yaml"}, "gimli.minecraft.example.com\n\n"},
33 {[]string{"public-address", "--format", "json"}, `"gimli.minecraft.example.com"` + "\n"},
34}
35
36func (s *UnitGetSuite) TestOutputFormat(c *C) {
37 for _, t := range unitGetTests {
38 com, err := s.ctx.NewCommand("unit-get")
39 c.Assert(err, IsNil)
40 ctx := dummyContext(c)
41 code := cmd.Main(com, ctx, t.args)
42 c.Assert(code, Equals, 0)
43 c.Assert(bufferString(ctx.Stderr), Equals, "")
44 c.Assert(bufferString(ctx.Stdout), Matches, t.out)
45 }
46}
47
48func (s *UnitGetSuite) TestTestMode(c *C) {
49 for _, key := range []string{"public-address", "private-address"} {
50 com, err := s.ctx.NewCommand("unit-get")
51 c.Assert(err, IsNil)
52 ctx := dummyContext(c)
53 code := cmd.Main(com, ctx, []string{"--test", key})
54 c.Assert(code, Equals, 0)
55 c.Assert(bufferString(ctx.Stderr), Equals, "")
56 c.Assert(bufferString(ctx.Stdout), Equals, "")
57 }
58}
59
60func (s *UnitGetSuite) TestHelp(c *C) {
61 com, err := s.ctx.NewCommand("unit-get")
62 c.Assert(err, IsNil)
63 ctx := dummyContext(c)
64 code := cmd.Main(com, ctx, []string{"--help"})
65 c.Assert(code, Equals, 0)
66 c.Assert(bufferString(ctx.Stdout), Equals, "")
67 c.Assert(bufferString(ctx.Stderr), Equals, `usage: unit-get [options] <setting>
68purpose: print public-address or private-address
69
70options:
71--format (= yaml)
72 specify output format (json|yaml)
73-o, --output (= "")
74 specify an output file
75--test (= false)
76 returns non-zero exit code if value is false/zero/empty
77`)
78}
79
80func (s *UnitGetSuite) TestOutputPath(c *C) {
81 com, err := s.ctx.NewCommand("unit-get")
82 c.Assert(err, IsNil)
83 ctx := dummyContext(c)
84 code := cmd.Main(com, ctx, []string{"--output", "some-file", "private-address"})
85 c.Assert(code, Equals, 0)
86 c.Assert(bufferString(ctx.Stderr), Equals, "")
87 c.Assert(bufferString(ctx.Stdout), Equals, "")
88 content, err := ioutil.ReadFile(filepath.Join(ctx.Dir, "some-file"))
89 c.Assert(err, IsNil)
90 c.Assert(string(content), Equals, "192.168.0.99\n\n")
91}
92
93func (s *UnitGetSuite) TestUnknownSetting(c *C) {
94 com, err := s.ctx.NewCommand("unit-get")
95 c.Assert(err, IsNil)
96 err = com.Init(dummyFlagSet(), []string{"protected-address"})
97 c.Assert(err, ErrorMatches, `unknown setting "protected-address"`)
98}
99
100func (s *UnitGetSuite) TestUnknownArg(c *C) {
101 com, err := s.ctx.NewCommand("unit-get")
102 c.Assert(err, IsNil)
103 err = com.Init(dummyFlagSet(), []string{"private-address", "blah"})
104 c.Assert(err, ErrorMatches, `unrecognized args: \["blah"\]`)
105}
106
107func (s *UnitGetSuite) TestUnitCommand(c *C) {
108 s.AssertUnitCommand(c, "unit-get")
109}
0110
=== added file 'cmd/jujuc/server/util_test.go.OTHER'
--- cmd/jujuc/server/util_test.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/jujuc/server/util_test.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,149 @@
1package server_test
2
3import (
4 "bytes"
5 "fmt"
6 "io"
7 . "launchpad.net/gocheck"
8 "launchpad.net/gozk/zookeeper"
9 "launchpad.net/juju-core/juju/charm"
10 "launchpad.net/juju-core/juju/cmd"
11 "launchpad.net/juju-core/juju/cmd/jujuc/server"
12 "launchpad.net/juju-core/juju/state"
13 "launchpad.net/juju-core/juju/testing"
14 "net/url"
15 stdtesting "testing"
16)
17
18var zkAddr string
19
20func TestPackage(t *stdtesting.T) {
21 srv := testing.StartZkServer()
22 defer srv.Destroy()
23 var err error
24 zkAddr, err = srv.Addr()
25 if err != nil {
26 t.Fatalf("could not get ZooKeeper server address")
27 }
28 TestingT(t)
29}
30
31func addDummyCharm(c *C, st *state.State) *state.Charm {
32 ch := testing.Charms.Dir("dummy")
33 u := fmt.Sprintf("local:series/%s-%d", ch.Meta().Name, ch.Revision())
34 curl := charm.MustParseURL(u)
35 burl, err := url.Parse("http://bundle.url")
36 c.Assert(err, IsNil)
37 dummy, err := st.AddCharm(ch, curl, burl, "dummy-sha256")
38 c.Assert(err, IsNil)
39 return dummy
40}
41
42func dummyContext(c *C) *cmd.Context {
43 return &cmd.Context{c.MkDir(), &bytes.Buffer{}, &bytes.Buffer{}}
44}
45
46func bufferString(w io.Writer) string {
47 return w.(*bytes.Buffer).String()
48}
49
50type UnitFixture struct {
51 ctx *server.ClientContext
52 service *state.Service
53 unit *state.Unit
54}
55
56func (f *UnitFixture) SetUpTest(c *C) {
57 st, err := state.Initialize(&state.Info{
58 Addrs: []string{zkAddr},
59 })
60 c.Assert(err, IsNil)
61 f.ctx = &server.ClientContext{
62 Id: "TestCtx",
63 State: st,
64 LocalUnitName: "minecraft/0",
65 }
66 dummy := addDummyCharm(c, st)
67 f.service, err = st.AddService("minecraft", dummy)
68 c.Assert(err, IsNil)
69 f.unit, err = f.service.AddUnit()
70 c.Assert(err, IsNil)
71}
72
73func (f *UnitFixture) TearDownTest(c *C) {
74 zk, session, err := zookeeper.Dial(zkAddr, 15e9)
75 c.Assert(err, IsNil)
76 event := <-session
77 c.Assert(event.Ok(), Equals, true)
78 c.Assert(event.Type, Equals, zookeeper.EVENT_SESSION)
79 c.Assert(event.State, Equals, zookeeper.STATE_CONNECTED)
80 testing.ZkRemoveTree(zk, "/")
81}
82
83func (f *UnitFixture) AssertUnitCommand(c *C, name string) {
84 ctx := &server.ClientContext{Id: "TestCtx", State: f.ctx.State}
85 com, err := ctx.NewCommand(name)
86 c.Assert(com, IsNil)
87 c.Assert(err, ErrorMatches, "context TestCtx is not attached to a unit")
88
89 ctx = &server.ClientContext{Id: "TestCtx", LocalUnitName: f.ctx.LocalUnitName}
90 com, err = ctx.NewCommand(name)
91 c.Assert(com, IsNil)
92 c.Assert(err, ErrorMatches, "context TestCtx cannot access state")
93}
94
95type TruthErrorSuite struct{}
96
97var _ = Suite(&TruthErrorSuite{})
98
99var truthErrorTests = []struct {
100 value interface{}
101 err error
102}{
103 {0, cmd.ErrSilent},
104 {int8(0), cmd.ErrSilent},
105 {int16(0), cmd.ErrSilent},
106 {int32(0), cmd.ErrSilent},
107 {int64(0), cmd.ErrSilent},
108 {uint(0), cmd.ErrSilent},
109 {uint8(0), cmd.ErrSilent},
110 {uint16(0), cmd.ErrSilent},
111 {uint32(0), cmd.ErrSilent},
112 {uint64(0), cmd.ErrSilent},
113 {uintptr(0), cmd.ErrSilent},
114 {123, nil},
115 {int8(123), nil},
116 {int16(123), nil},
117 {int32(123), nil},
118 {int64(123), nil},
119 {uint(123), nil},
120 {uint8(123), nil},
121 {uint16(123), nil},
122 {uint32(123), nil},
123 {uint64(123), nil},
124 {uintptr(123), nil},
125 {0.0, cmd.ErrSilent},
126 {float32(0.0), cmd.ErrSilent},
127 {123.45, nil},
128 {float32(123.45), nil},
129 {nil, cmd.ErrSilent},
130 {"", cmd.ErrSilent},
131 {"blah", nil},
132 {true, nil},
133 {false, cmd.ErrSilent},
134 {[]string{}, cmd.ErrSilent},
135 {[]string{""}, nil},
136 {[]bool{}, cmd.ErrSilent},
137 {[]bool{false}, nil},
138 {map[string]string{}, cmd.ErrSilent},
139 {map[string]string{"": ""}, nil},
140 {map[bool]bool{}, cmd.ErrSilent},
141 {map[bool]bool{false: false}, nil},
142 {struct{ x bool }{false}, nil},
143}
144
145func (s *TruthErrorSuite) TestTruthError(c *C) {
146 for _, t := range truthErrorTests {
147 c.Assert(server.TruthError(t.value), Equals, t.err)
148 }
149}
0150
=== added directory 'cmd/jujud'
=== added file 'cmd/jujud/agent.go.OTHER'
--- cmd/jujud/agent.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/jujud/agent.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,70 @@
1package main
2
3import (
4 "fmt"
5 "launchpad.net/gnuflag"
6 "launchpad.net/juju-core/juju/cmd"
7 "launchpad.net/juju-core/juju/state"
8 "regexp"
9 "strings"
10)
11
12// requiredError is useful when complaining about missing command-line options.
13func requiredError(name string) error {
14 return fmt.Errorf("--%s option must be set", name)
15}
16
17// stateInfoValue implements gnuflag.Value on a state.Info.
18type stateInfoValue state.Info
19
20var validAddr = regexp.MustCompile("^.+:[0-9]+$")
21
22// Set splits the comma-separated list of ZooKeeper addresses and stores
23// onto v's Addrs. Addresses must include port numbers.
24func (v *stateInfoValue) Set(value string) error {
25 addrs := strings.Split(value, ",")
26 for _, addr := range addrs {
27 if !validAddr.MatchString(addr) {
28 return fmt.Errorf("%q is not a valid zookeeper address", addr)
29 }
30 }
31 v.Addrs = addrs
32 return nil
33}
34
35// String returns the list of ZooKeeper addresses joined by commas.
36func (v *stateInfoValue) String() string {
37 if v.Addrs != nil {
38 return strings.Join(v.Addrs, ",")
39 }
40 return ""
41}
42
43// stateInfoVar sets up a gnuflag flag analagously to FlagSet.*Var methods.
44func stateInfoVar(fs *gnuflag.FlagSet, target *state.Info, name string, value []string, usage string) {
45 target.Addrs = value
46 fs.Var((*stateInfoValue)(target), name, usage)
47}
48
49// AgentConf handles command-line flags shared by all agents.
50type AgentConf struct {
51 JujuDir string // Defaults to "/var/lib/juju".
52 StateInfo state.Info
53}
54
55// addFlags injects common agent flags into f.
56func (c *AgentConf) addFlags(f *gnuflag.FlagSet) {
57 f.StringVar(&c.JujuDir, "juju-directory", "/var/lib/juju", "juju working directory")
58 stateInfoVar(f, &c.StateInfo, "zookeeper-servers", nil, "zookeeper servers to connect to")
59}
60
61// checkArgs checks that required flags have been set and that args is empty.
62func (c *AgentConf) checkArgs(args []string) error {
63 if c.JujuDir == "" {
64 return requiredError("juju-directory")
65 }
66 if c.StateInfo.Addrs == nil {
67 return requiredError("zookeeper-servers")
68 }
69 return cmd.CheckEmpty(args)
70}
071
=== added file 'cmd/jujud/initzk.go.OTHER'
--- cmd/jujud/initzk.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/jujud/initzk.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,44 @@
1package main
2
3import (
4 "launchpad.net/gnuflag"
5 "launchpad.net/juju-core/juju/cmd"
6 "launchpad.net/juju-core/juju/state"
7)
8
9type InitzkCommand struct {
10 StateInfo state.Info
11 InstanceId string
12 EnvType string
13}
14
15// Info returns a decription of the command.
16func (c *InitzkCommand) Info() *cmd.Info {
17 return &cmd.Info{"initzk", "", "initialize juju state in a local zookeeper", ""}
18}
19
20// Init initializes the command for running.
21func (c *InitzkCommand) Init(f *gnuflag.FlagSet, args []string) error {
22 stateInfoVar(f, &c.StateInfo, "zookeeper-servers", []string{"127.0.0.1:2181"}, "address of zookeeper to initialize")
23 f.StringVar(&c.InstanceId, "instance-id", "", "instance id of this machine")
24 f.StringVar(&c.EnvType, "env-type", "", "environment type")
25 if err := f.Parse(true, args); err != nil {
26 return err
27 }
28 if c.StateInfo.Addrs == nil {
29 return requiredError("zookeeper-servers")
30 }
31 if c.InstanceId == "" {
32 return requiredError("instance-id")
33 }
34 if c.EnvType == "" {
35 return requiredError("env-type")
36 }
37 return cmd.CheckEmpty(f.Args())
38}
39
40// Run initializes zookeeper state for an environment.
41func (c *InitzkCommand) Run(_ *cmd.Context) error {
42 _, err := state.Initialize(&c.StateInfo)
43 return err
44}
045
=== added file 'cmd/jujud/initzk_test.go.OTHER'
--- cmd/jujud/initzk_test.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/jujud/initzk_test.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,78 @@
1package main
2
3import (
4 . "launchpad.net/gocheck"
5 "launchpad.net/gozk/zookeeper"
6 "launchpad.net/juju-core/juju/testing"
7 stdtesting "testing"
8)
9
10var zkAddr string
11
12func TestPackage(t *stdtesting.T) {
13 srv := testing.StartZkServer()
14 defer srv.Destroy()
15 var err error
16 zkAddr, err = srv.Addr()
17 if err != nil {
18 t.Fatalf("could not get ZooKeeper server address")
19 }
20 TestingT(t)
21}
22
23type InitzkSuite struct {
24 zkConn *zookeeper.Conn
25 path string
26}
27
28var _ = Suite(&InitzkSuite{})
29
30func (s *InitzkSuite) SetUpTest(c *C) {
31 zk, session, err := zookeeper.Dial(zkAddr, 15e9)
32 c.Assert(err, IsNil)
33 event := <-session
34 c.Assert(event.Ok(), Equals, true)
35 c.Assert(event.Type, Equals, zookeeper.EVENT_SESSION)
36 c.Assert(event.State, Equals, zookeeper.STATE_CONNECTED)
37
38 s.zkConn = zk
39 s.path = "/watcher"
40
41 c.Assert(err, IsNil)
42}
43
44func (s *InitzkSuite) TearDownTest(c *C) {
45 testing.ZkRemoveTree(s.zkConn, s.path)
46 s.zkConn.Close()
47}
48
49func initInitzkCommand(args []string) (*InitzkCommand, error) {
50 c := &InitzkCommand{}
51 return c, initCmd(c, args)
52}
53
54func (s *InitzkSuite) TestParse(c *C) {
55 args := []string{}
56 _, err := initInitzkCommand(args)
57 c.Assert(err, ErrorMatches, "--instance-id option must be set")
58
59 args = append(args, "--instance-id", "iWhatever")
60 _, err = initInitzkCommand(args)
61 c.Assert(err, ErrorMatches, "--env-type option must be set")
62
63 args = append(args, "--env-type", "dummy")
64 izk, err := initInitzkCommand(args)
65 c.Assert(err, IsNil)
66 c.Assert(izk.StateInfo.Addrs, DeepEquals, []string{"127.0.0.1:2181"})
67 c.Assert(izk.InstanceId, Equals, "iWhatever")
68 c.Assert(izk.EnvType, Equals, "dummy")
69
70 args = append(args, "--zookeeper-servers", "zk1:2181,zk2:2181")
71 izk, err = initInitzkCommand(args)
72 c.Assert(err, IsNil)
73 c.Assert(izk.StateInfo.Addrs, DeepEquals, []string{"zk1:2181", "zk2:2181"})
74
75 args = append(args, "haha disregard that")
76 _, err = initInitzkCommand(args)
77 c.Assert(err, ErrorMatches, `unrecognized args: \["haha disregard that"\]`)
78}
079
=== added file 'cmd/jujud/machine.go.OTHER'
--- cmd/jujud/machine.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/jujud/machine.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,36 @@
1package main
2
3import (
4 "fmt"
5 "launchpad.net/gnuflag"
6 "launchpad.net/juju-core/juju/cmd"
7)
8
9// MachineAgent is a cmd.Command responsible for running a machine agent.
10type MachineAgent struct {
11 Conf AgentConf
12 MachineId int
13}
14
15// Info returns usage information for the command.
16func (a *MachineAgent) Info() *cmd.Info {
17 return &cmd.Info{"machine", "", "run a juju machine agent", ""}
18}
19
20// Init initializes the command for running.
21func (a *MachineAgent) Init(f *gnuflag.FlagSet, args []string) error {
22 a.Conf.addFlags(f)
23 f.IntVar(&a.MachineId, "machine-id", -1, "id of the machine to run")
24 if err := f.Parse(true, args); err != nil {
25 return err
26 }
27 if a.MachineId < 0 {
28 return fmt.Errorf("--machine-id option must be set, and expects a non-negative integer")
29 }
30 return a.Conf.checkArgs(f.Args())
31}
32
33// Run runs a machine agent.
34func (a *MachineAgent) Run(_ *cmd.Context) error {
35 return fmt.Errorf("MachineAgent.Run not implemented")
36}
037
=== added file 'cmd/jujud/machine_test.go.OTHER'
--- cmd/jujud/machine_test.go.OTHER 1970-01-01 00:00:00 +0000
+++ cmd/jujud/machine_test.go.OTHER 2012-06-07 11:57:18 +0000
@@ -0,0 +1,35 @@
1package main
2
3import (
4 . "launchpad.net/gocheck"
5 "launchpad.net/juju-core/juju/cmd"
6)
7
8type MachineSuite struct{}
9
10var _ = Suite(&MachineSuite{})
11
12func (s *MachineSuite) TestParseSuccess(c *C) {
13 create := func() (cmd.Command, *AgentConf) {
14 a := &MachineAgent{}
15 return a, &a.Conf
16 }
17 a := CheckAgentCommand(c, create, []string{"--machine-id", "42"})
18 c.Assert(a.(*MachineAgent).MachineId, Equals, 42)
19}
20
21func (s *MachineSuite) TestParseNonsense(c *C) {
22 for _, args := range [][]string{
23 []string{},
24 []string{"--machine-id", "-4004"},
25 } {
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: