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