Merge lp:~fwereade/pyjuju/go-add-control-package into lp:pyjuju

Proposed by William Reade
Status: Work in progress
Proposed branch: lp:~fwereade/pyjuju/go-add-control-package
Merge into: lp:pyjuju
Diff against target: 7112 lines (+6606/-0) (has conflicts)
92 files modified
.bzrignore (+5/-0)
.lbox (+1/-0)
charm/Makefile (+28/-0)
charm/bundle.go (+222/-0)
charm/bundle_test.go (+122/-0)
charm/charm.go (+9/-0)
charm/charm_test.go (+58/-0)
charm/config.go (+117/-0)
charm/config_test.go (+143/-0)
charm/dir.go (+174/-0)
charm/dir_test.go (+167/-0)
charm/export_test.go (+11/-0)
charm/meta.go (+163/-0)
charm/meta_test.go (+108/-0)
charm/testrepo/series/dummy/.ignored (+1/-0)
charm/testrepo/series/dummy/config.yaml (+5/-0)
charm/testrepo/series/dummy/hooks/install (+2/-0)
charm/testrepo/series/dummy/metadata.yaml (+5/-0)
charm/testrepo/series/dummy/revision (+1/-0)
charm/testrepo/series/dummy/src/hello.c (+7/-0)
charm/testrepo/series/mysql-alternative/metadata.yaml (+9/-0)
charm/testrepo/series/mysql-alternative/revision (+1/-0)
charm/testrepo/series/mysql/metadata.yaml (+5/-0)
charm/testrepo/series/mysql/revision (+1/-0)
charm/testrepo/series/new/metadata.yaml (+5/-0)
charm/testrepo/series/new/revision (+1/-0)
charm/testrepo/series/old/metadata.yaml (+5/-0)
charm/testrepo/series/old/revision (+1/-0)
charm/testrepo/series/riak/metadata.yaml (+11/-0)
charm/testrepo/series/riak/revision (+1/-0)
charm/testrepo/series/varnish-alternative/hooks/install (+3/-0)
charm/testrepo/series/varnish-alternative/metadata.yaml (+5/-0)
charm/testrepo/series/varnish-alternative/revision (+1/-0)
charm/testrepo/series/varnish/metadata.yaml (+5/-0)
charm/testrepo/series/varnish/revision (+1/-0)
charm/testrepo/series/wordpress/config.yaml (+3/-0)
charm/testrepo/series/wordpress/metadata.yaml (+19/-0)
charm/testrepo/series/wordpress/revision (+1/-0)
charm/url.go (+132/-0)
charm/url_test.go (+61/-0)
cloudinit/Makefile (+24/-0)
cloudinit/cloudinit.go (+66/-0)
cloudinit/cloudinit_test.go (+200/-0)
cloudinit/options.go (+214/-0)
control/bootstrap.go (+28/-0)
control/command.go (+69/-0)
control/command_test.go (+100/-0)
environs/Makefile (+25/-0)
environs/config.go (+134/-0)
environs/config_test.go (+179/-0)
environs/dummyprovider_test.go (+93/-0)
environs/ec2/Makefile (+31/-0)
environs/ec2/config.go (+79/-0)
environs/ec2/config_test.go (+104/-0)
environs/ec2/ec2.go (+171/-0)
environs/ec2/image.go (+80/-0)
environs/ec2/image_test.go (+138/-0)
environs/ec2/images/query/natty/desktop/daily.current.txt (+25/-0)
environs/ec2/images/query/natty/server/daily.current.txt (+25/-0)
environs/ec2/images/query/natty/server/released.current.txt (+25/-0)
environs/ec2/images/query/oneiric/desktop/daily.current.txt (+25/-0)
environs/ec2/images/query/oneiric/server/daily.current.txt (+25/-0)
environs/ec2/images/query/oneiric/server/released.current.txt (+25/-0)
environs/ec2/live_test.go (+34/-0)
environs/ec2/local_test.go (+242/-0)
environs/ec2/suite_test.go (+25/-0)
environs/ec2/util.go (+43/-0)
environs/interface.go (+42/-0)
environs/jujutest/Makefile (+25/-0)
environs/jujutest/jujutest_test.go (+8/-0)
environs/jujutest/livetests.go (+52/-0)
environs/jujutest/test.go (+66/-0)
environs/jujutest/tests.go (+31/-0)
environs/open.go (+28/-0)
environs/suite_test.go (+14/-0)
log/Makefile (+23/-0)
log/log.go (+27/-0)
log/log_test.go (+54/-0)
schema/Makefile (+23/-0)
schema/schema.go (+374/-0)
schema/schema_test.go (+292/-0)
state/Makefile (+27/-0)
state/service.go (+189/-0)
state/state.go (+51/-0)
state/state_test.go (+244/-0)
state/topology.go (+240/-0)
state/unit.go (+80/-0)
state/util.go (+60/-0)
store/Makefile (+22/-0)
store/mgo_test.go (+98/-0)
store/store.go (+425/-0)
store/store_test.go (+262/-0)
Conflict adding file .bzrignore.  Moved existing file to .bzrignore.moved.
To merge this branch: bzr merge lp:~fwereade/pyjuju/go-add-control-package
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+89077@code.launchpad.net

Description of the change

First flailing attempts at using go

Profoundly incomplete control package for juju, with a few tests. Usage
output is definitely broken; I'm not aware of anything else that is actively
*wrong*. Not using gnu-style flags package yet.

To post a comment you must log in.
38. By William Reade

merge go-add-control-package

Unmerged revisions

38. By William Reade

merge go-add-control-package

37. By Gustavo Niemeyer

build: remove all.bash and fix .lbox

R=rog
CC=
https://codereview.appspot.com/5539069

36. By Roger Peppe

environs/ec2: launch instances in named security groups

R=niemeyer
CC=
https://codereview.appspot.com/5532058

35. By Gustavo Niemeyer

store: base support for storing charms

R=
CC=
https://codereview.appspot.com/5504047

34. By Gustavo Niemeyer

store: save charm config and meta

This turns CharmInfo into a charm.Charm!

R=rog
CC=
https://codereview.appspot.com/5532049

33. By Frank Mueller

Initial adding of the state model to the Go port of juju

As a first step in adding the Go port the state can be opened and first
operations ond Service and Unit can be done. Also needed first topology
functionality is part of this branch.

R=
CC=
https://codereview.appspot.com/5502043

32. By Roger Peppe

environs/ec2test: add new test suite for live tests

R=niemeyer
CC=
https://codereview.appspot.com/5490073

31. By Gustavo Niemeyer

charm: must handle new revision schema

Revision handling was moved out of the metadata.yaml file.
The Go port most be updated to handle it.

This branch implements the new schema for charm revision handling
in an independent file, including backwards compatibility with
the previous schema, and also SetVersion methods that enable
bundling and expanding charms with custom revisions
(necessary for store).

R=rog
CC=
https://codereview.appspot.com/5489087

30. By Roger Peppe

juju: rename to "environs"

R=niemeyer
CC=
https://codereview.appspot.com/5495052

29. By Roger Peppe

merge go-juju-ec2-operations

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file '.bzrignore'
2--- .bzrignore 1970-01-01 00:00:00 +0000
3+++ .bzrignore 2012-01-18 17:30:30 +0000
4@@ -0,0 +1,5 @@
5+*.6
6+_obj
7+_test
8+6.out
9+_testmain.go
10
11=== renamed file '.bzrignore' => '.bzrignore.moved'
12=== added file '.lbox'
13--- .lbox 1970-01-01 00:00:00 +0000
14+++ .lbox 2012-01-18 17:30:30 +0000
15@@ -0,0 +1,1 @@
16+propose -cr -for lp:juju/go
17
18=== added directory 'charm'
19=== added file 'charm/Makefile'
20--- charm/Makefile 1970-01-01 00:00:00 +0000
21+++ charm/Makefile 2012-01-18 17:30:30 +0000
22@@ -0,0 +1,28 @@
23+include $(GOROOT)/src/Make.inc
24+
25+all: package
26+
27+TARG=launchpad.net/juju/go/charm
28+
29+GOFILES=\
30+ bundle.go\
31+ config.go\
32+ dir.go\
33+ charm.go\
34+ meta.go\
35+ url.go\
36+
37+GOFMT=gofmt
38+BADFMT:=$(shell $(GOFMT) -l $(GOFILES) $(CGOFILES) $(wildcard *_test.go))
39+
40+gofmt: $(BADFMT)
41+ @for F in $(BADFMT); do $(GOFMT) -w $$F && echo $$F; done
42+
43+ifneq ($(BADFMT),)
44+ifneq ($(MAKECMDGOALS),gofmt)
45+$(warning WARNING: make gofmt: $(BADFMT))
46+endif
47+endif
48+
49+include $(GOROOT)/src/Make.pkg
50+
51
52=== added file 'charm/bundle.go'
53--- charm/bundle.go 1970-01-01 00:00:00 +0000
54+++ charm/bundle.go 2012-01-18 17:30:30 +0000
55@@ -0,0 +1,222 @@
56+package charm
57+
58+import (
59+ "archive/zip"
60+ "errors"
61+ "fmt"
62+ "io"
63+ "os"
64+ "path/filepath"
65+ "strconv"
66+ "strings"
67+)
68+
69+// The Bundle type encapsulates access to data and operations
70+// on a charm bundle.
71+type Bundle struct {
72+ Path string // May be empty if Bundle wasn't read from a file
73+ meta *Meta
74+ config *Config
75+ revision int
76+ r io.ReaderAt
77+ size int64
78+}
79+
80+// Trick to ensure *Bundle implements the Charm interface.
81+var _ Charm = (*Bundle)(nil)
82+
83+// ReadBundle returns a Bundle for the charm in path.
84+func ReadBundle(path string) (bundle *Bundle, err error) {
85+ f, err := os.Open(path)
86+ if err != nil {
87+ return
88+ }
89+ defer f.Close()
90+ fi, err := f.Stat()
91+ if err != nil {
92+ return
93+ }
94+ b, err := readBundle(f, fi.Size())
95+ if err != nil {
96+ return
97+ }
98+ b.Path = path
99+ return b, nil
100+}
101+
102+// ReadBundleBytes returns a Bundle read from the given data.
103+// Make sure the bundle fits in memory before using this.
104+func ReadBundleBytes(data []byte) (bundle *Bundle, err error) {
105+ return readBundle(readAtBytes(data), int64(len(data)))
106+}
107+
108+func readBundle(r io.ReaderAt, size int64) (bundle *Bundle, err error) {
109+ b := &Bundle{r: r, size: size}
110+ zipr, err := zip.NewReader(r, size)
111+ if err != nil {
112+ return
113+ }
114+
115+ reader, err := zipOpen(zipr, "metadata.yaml")
116+ if err != nil {
117+ return
118+ }
119+ b.meta, err = ReadMeta(reader)
120+ reader.Close()
121+ if err != nil {
122+ return
123+ }
124+
125+ reader, err = zipOpen(zipr, "config.yaml")
126+ if err != nil {
127+ return
128+ }
129+ b.config, err = ReadConfig(reader)
130+ reader.Close()
131+ if err != nil {
132+ return
133+ }
134+
135+ reader, err = zipOpen(zipr, "revision")
136+ if err != nil {
137+ if _, ok := err.(noBundleFile); !ok {
138+ return
139+ }
140+ b.revision = b.meta.OldRevision
141+ } else {
142+ _, err = fmt.Fscan(reader, &b.revision)
143+ if err != nil {
144+ return nil, errors.New("invalid revision file")
145+ }
146+ }
147+ return b, nil
148+}
149+
150+func zipOpen(zipr *zip.Reader, path string) (rc io.ReadCloser, err error) {
151+ for _, fh := range zipr.File {
152+ if fh.Name == path {
153+ return fh.Open()
154+ }
155+ }
156+ return nil, noBundleFile{path}
157+}
158+
159+type noBundleFile struct {
160+ path string
161+}
162+
163+func (err noBundleFile) Error() string {
164+ return fmt.Sprintf("bundle file not found: %s", err.path)
165+}
166+
167+// Revision returns the revision number for the charm
168+// expanded in dir.
169+func (b *Bundle) Revision() int {
170+ return b.revision
171+}
172+
173+// SetRevision changes the charm revision number. This affects the
174+// revision reported by Revision and the revision of the charm
175+// directory created by ExpandTo.
176+func (b *Bundle) SetRevision(revision int) {
177+ b.revision = revision
178+}
179+
180+// Meta returns the Meta representing the metadata.yaml file from bundle.
181+func (b *Bundle) Meta() *Meta {
182+ return b.meta
183+}
184+
185+// Config returns the Config representing the config.yaml file
186+// for the charm bundle.
187+func (b *Bundle) Config() *Config {
188+ return b.config
189+}
190+
191+// ExpandTo expands the charm bundle into dir, creating it if necessary.
192+// If any errors occur during the expansion procedure, the process will
193+// continue. Only the last error found is returned.
194+func (b *Bundle) ExpandTo(dir string) (err error) {
195+ // If we have a Path, reopen the file. Otherwise, try to use
196+ // the original ReaderAt.
197+ r := b.r
198+ size := b.size
199+ if b.Path != "" {
200+ f, err := os.Open(b.Path)
201+ if err != nil {
202+ return err
203+ }
204+ defer f.Close()
205+ fi, err := f.Stat()
206+ if err != nil {
207+ return err
208+ }
209+ r = f
210+ size = fi.Size()
211+ }
212+
213+ zipr, err := zip.NewReader(r, size)
214+ if err != nil {
215+ return err
216+ }
217+
218+ var lasterr error
219+ for _, zfile := range zipr.File {
220+ if err := b.expand(dir, zfile); err != nil {
221+ lasterr = err
222+ }
223+ }
224+
225+ revFile, err := os.Create(filepath.Join(dir, "revision"))
226+ if err != nil {
227+ return err
228+ }
229+ _, err = revFile.Write([]byte(strconv.Itoa(b.revision)))
230+ revFile.Close()
231+ if err != nil {
232+ return err
233+ }
234+ return lasterr
235+}
236+
237+func (b *Bundle) expand(dir string, zfile *zip.File) error {
238+ cleanName := filepath.Clean(zfile.Name)
239+ if cleanName == "revision" {
240+ return nil
241+ }
242+
243+ r, err := zfile.Open()
244+ if err != nil {
245+ return err
246+ }
247+ defer r.Close()
248+
249+ path := filepath.Join(dir, cleanName)
250+ if strings.HasSuffix(zfile.Name, "/") {
251+ err = os.MkdirAll(path, 0755)
252+ if err != nil {
253+ return err
254+ }
255+ return nil
256+ }
257+
258+ base, _ := filepath.Split(path)
259+ err = os.MkdirAll(base, 0755)
260+ if err != nil {
261+ return err
262+ }
263+ f, err := os.Create(path)
264+ if err != nil {
265+ return err
266+ }
267+ _, err = io.Copy(f, r)
268+ f.Close()
269+ return err
270+}
271+
272+// FWIW, being able to do this is awesome.
273+type readAtBytes []byte
274+
275+func (b readAtBytes) ReadAt(out []byte, off int64) (n int, err error) {
276+ return copy(out, b[off:]), nil
277+}
278
279=== added file 'charm/bundle_test.go'
280--- charm/bundle_test.go 1970-01-01 00:00:00 +0000
281+++ charm/bundle_test.go 2012-01-18 17:30:30 +0000
282@@ -0,0 +1,122 @@
283+package charm_test
284+
285+import (
286+ "fmt"
287+ "io/ioutil"
288+ . "launchpad.net/gocheck"
289+ "launchpad.net/juju/go/charm"
290+ "os"
291+ "os/exec"
292+ "path/filepath"
293+)
294+
295+type BundleSuite struct {
296+ bundlePath string
297+}
298+
299+var _ = Suite(&BundleSuite{})
300+
301+func (s *BundleSuite) SetUpSuite(c *C) {
302+ s.bundlePath = bundleDir(c, repoDir("dummy"))
303+}
304+
305+func (s *BundleSuite) TestReadBundle(c *C) {
306+ bundle, err := charm.ReadBundle(s.bundlePath)
307+ c.Assert(err, IsNil)
308+ checkDummy(c, bundle, s.bundlePath)
309+}
310+
311+func (s *BundleSuite) TestReadBundleBytes(c *C) {
312+ data, err := ioutil.ReadFile(s.bundlePath)
313+ c.Assert(err, IsNil)
314+
315+ bundle, err := charm.ReadBundleBytes(data)
316+ c.Assert(err, IsNil)
317+ checkDummy(c, bundle, "")
318+}
319+
320+func (s *BundleSuite) TestExpandTo(c *C) {
321+ bundle, err := charm.ReadBundle(s.bundlePath)
322+ c.Assert(err, IsNil)
323+
324+ path := filepath.Join(c.MkDir(), "charm")
325+ err = bundle.ExpandTo(path)
326+ c.Assert(err, IsNil)
327+
328+ dir, err := charm.ReadDir(path)
329+ c.Assert(err, IsNil)
330+ checkDummy(c, dir, path)
331+}
332+
333+func (s *BundleSuite) TestBundleRevisionFile(c *C) {
334+ charmDir := c.MkDir()
335+ copyCharmDir(charmDir, repoDir("dummy"))
336+ revPath := filepath.Join(charmDir, "revision")
337+
338+ // Missing revision file
339+ err := os.Remove(revPath)
340+ c.Assert(err, IsNil)
341+
342+ bundle, err := charm.ReadBundle(extBundleDir(c, charmDir))
343+ c.Assert(err, IsNil)
344+ c.Assert(bundle.Revision(), Equals, 0)
345+
346+ // Missing revision file with old revision in metadata
347+ file, err := os.OpenFile(filepath.Join(charmDir, "metadata.yaml"), os.O_WRONLY|os.O_APPEND, 0)
348+ c.Assert(err, IsNil)
349+ _, err = file.Write([]byte("\nrevision: 1234\n"))
350+ c.Assert(err, IsNil)
351+
352+ bundle, err = charm.ReadBundle(extBundleDir(c, charmDir))
353+ c.Assert(err, IsNil)
354+ c.Assert(bundle.Revision(), Equals, 1234)
355+
356+ // Revision file with bad content
357+ err = ioutil.WriteFile(revPath, []byte("garbage"), 0666)
358+ c.Assert(err, IsNil)
359+
360+ bundle, err = charm.ReadBundle(extBundleDir(c, charmDir))
361+ c.Assert(err, ErrorMatches, "invalid revision file")
362+ c.Assert(bundle, IsNil)
363+}
364+
365+func (s *BundleSuite) TestBundleSetRevision(c *C) {
366+ bundle, err := charm.ReadBundle(s.bundlePath)
367+ c.Assert(err, IsNil)
368+
369+ c.Assert(bundle.Revision(), Equals, 1)
370+ bundle.SetRevision(42)
371+ c.Assert(bundle.Revision(), Equals, 42)
372+
373+ path := filepath.Join(c.MkDir(), "charm")
374+ err = bundle.ExpandTo(path)
375+ c.Assert(err, IsNil)
376+
377+ dir, err := charm.ReadDir(path)
378+ c.Assert(err, IsNil)
379+ c.Assert(dir.Revision(), Equals, 42)
380+}
381+
382+func bundleDir(c *C, dirpath string) (path string) {
383+ dir, err := charm.ReadDir(dirpath)
384+ c.Assert(err, IsNil)
385+
386+ path = filepath.Join(c.MkDir(), "bundle.charm")
387+
388+ file, err := os.Create(path)
389+ c.Assert(err, IsNil)
390+
391+ err = dir.BundleTo(file)
392+ c.Assert(err, IsNil)
393+ file.Close()
394+
395+ return path
396+}
397+
398+func extBundleDir(c *C, dirpath string) (path string) {
399+ path = filepath.Join(c.MkDir(), "bundle.charm")
400+ cmd := exec.Command("/bin/sh", "-c", fmt.Sprintf("cd %s; zip -r %s .", dirpath, path))
401+ output, err := cmd.CombinedOutput()
402+ c.Assert(err, IsNil, Bug("Command output: %s", output))
403+ return path
404+}
405
406=== added file 'charm/charm.go'
407--- charm/charm.go 1970-01-01 00:00:00 +0000
408+++ charm/charm.go 2012-01-18 17:30:30 +0000
409@@ -0,0 +1,9 @@
410+package charm
411+
412+// The Charm interface is implemented by any type that
413+// may be handled as a charm.
414+type Charm interface {
415+ Meta() *Meta
416+ Config() *Config
417+ Revision() int
418+}
419
420=== added file 'charm/charm_test.go'
421--- charm/charm_test.go 1970-01-01 00:00:00 +0000
422+++ charm/charm_test.go 2012-01-18 17:30:30 +0000
423@@ -0,0 +1,58 @@
424+package charm_test
425+
426+import (
427+ "bytes"
428+ "io"
429+ "io/ioutil"
430+ . "launchpad.net/gocheck"
431+ "launchpad.net/goyaml"
432+ "launchpad.net/juju/go/charm"
433+ "os"
434+ "path/filepath"
435+ "testing"
436+)
437+
438+func Test(t *testing.T) {
439+ TestingT(t)
440+}
441+
442+type S struct{}
443+
444+var _ = Suite(&S{})
445+
446+func checkDummy(c *C, f charm.Charm, path string) {
447+ c.Assert(f.Revision(), Equals, 1)
448+ c.Assert(f.Meta().Name, Equals, "dummy")
449+ c.Assert(f.Config().Options["title"].Default, Equals, "My Title")
450+ switch f := f.(type) {
451+ case *charm.Bundle:
452+ c.Assert(f.Path, Equals, path)
453+ case *charm.Dir:
454+ c.Assert(f.Path, Equals, path)
455+ _, err := os.Stat(filepath.Join(path, "src", "hello.c"))
456+ c.Assert(err, IsNil)
457+ }
458+}
459+
460+type YamlHacker map[interface{}]interface{}
461+
462+func ReadYaml(r io.Reader) YamlHacker {
463+ data, err := ioutil.ReadAll(r)
464+ if err != nil {
465+ panic(err)
466+ }
467+ m := make(map[interface{}]interface{})
468+ err = goyaml.Unmarshal(data, m)
469+ if err != nil {
470+ panic(err)
471+ }
472+ return YamlHacker(m)
473+}
474+
475+func (yh YamlHacker) Reader() io.Reader {
476+ data, err := goyaml.Marshal(yh)
477+ if err != nil {
478+ panic(err)
479+ }
480+ return bytes.NewBuffer(data)
481+}
482
483=== added file 'charm/config.go'
484--- charm/config.go 1970-01-01 00:00:00 +0000
485+++ charm/config.go 2012-01-18 17:30:30 +0000
486@@ -0,0 +1,117 @@
487+package charm
488+
489+import (
490+ "errors"
491+ "fmt"
492+ "io"
493+ "io/ioutil"
494+ "launchpad.net/goyaml"
495+ "launchpad.net/juju/go/schema"
496+ "strconv"
497+)
498+
499+// Option represents a single configuration option that is declared
500+// as supported by a charm in its config.yaml file.
501+type Option struct {
502+ Title string
503+ Description string
504+ Type string
505+ Default interface{}
506+}
507+
508+// Config represents the supported configuration options for a charm,
509+// as declared in its config.yaml file.
510+type Config struct {
511+ Options map[string]Option
512+}
513+
514+// ReadConfig reads a config.yaml file and returns its representation.
515+func ReadConfig(r io.Reader) (config *Config, err error) {
516+ data, err := ioutil.ReadAll(r)
517+ if err != nil {
518+ return
519+ }
520+ raw := make(map[interface{}]interface{})
521+ err = goyaml.Unmarshal(data, raw)
522+ if err != nil {
523+ return
524+ }
525+ v, err := configSchema.Coerce(raw, nil)
526+ if err != nil {
527+ return nil, errors.New("config: " + err.Error())
528+ }
529+ config = &Config{}
530+ config.Options = make(map[string]Option)
531+ m := v.(schema.MapType)
532+ for name, infov := range m["options"].(schema.MapType) {
533+ opt := infov.(schema.MapType)
534+ optTitle, _ := opt["title"].(string)
535+ optType, _ := opt["type"].(string)
536+ optDescr, _ := opt["description"].(string)
537+ optDefault, _ := opt["default"]
538+ config.Options[name.(string)] = Option{
539+ Title: optTitle,
540+ Type: optType,
541+ Description: optDescr,
542+ Default: optDefault,
543+ }
544+ }
545+ return
546+}
547+
548+// Validate processes the values in the input map according to the
549+// configuration in config, doing the following operations:
550+//
551+// - Values are converted from strings to the types defined
552+// - Options with default values are introduced for missing keys
553+// - Unknown keys and badly typed values are reported as errors
554+//
555+func (c *Config) Validate(values map[string]string) (processed map[string]interface{}, err error) {
556+ out := make(map[string]interface{})
557+ for k, v := range values {
558+ opt, ok := c.Options[k]
559+ if !ok {
560+ return nil, fmt.Errorf("Unknown configuration option: %q", k)
561+ }
562+ switch opt.Type {
563+ case "string":
564+ out[k] = v
565+ case "int":
566+ i, err := strconv.ParseInt(v, 10, 64)
567+ if err != nil {
568+ return nil, fmt.Errorf("Value for %q is not an int: %q", k, v)
569+ }
570+ out[k] = i
571+ case "float":
572+ f, err := strconv.ParseFloat(v, 64)
573+ if err != nil {
574+ return nil, fmt.Errorf("Value for %q is not a float: %q", k, v)
575+ }
576+ out[k] = f
577+ default:
578+ panic(fmt.Errorf("Internal error: option type %q is unknown to Validate", opt.Type))
579+ }
580+ }
581+ for k, opt := range c.Options {
582+ if _, ok := out[k]; !ok && opt.Default != nil {
583+ out[k] = opt.Default
584+ }
585+ }
586+ return out, nil
587+}
588+
589+var optionSchema = schema.FieldMap(
590+ schema.Fields{
591+ "type": schema.OneOf(schema.Const("string"), schema.Const("int"), schema.Const("float")),
592+ "default": schema.OneOf(schema.String(), schema.Int(), schema.Float()),
593+ "description": schema.String(),
594+ },
595+ schema.Optional{"default", "description"},
596+)
597+
598+var configSchema = schema.FieldMap(
599+ schema.Fields{
600+ "options": schema.Map(schema.String(), optionSchema),
601+ },
602+ nil,
603+)
604
605=== added file 'charm/config_test.go'
606--- charm/config_test.go 1970-01-01 00:00:00 +0000
607+++ charm/config_test.go 2012-01-18 17:30:30 +0000
608@@ -0,0 +1,143 @@
609+package charm_test
610+
611+import (
612+ "bytes"
613+ "io"
614+ "io/ioutil"
615+ . "launchpad.net/gocheck"
616+ "launchpad.net/juju/go/charm"
617+ "os"
618+ "path/filepath"
619+)
620+
621+var sampleConfig = `
622+options:
623+ title:
624+ default: My Title
625+ description: A descriptive title used for the service.
626+ type: string
627+ outlook:
628+ description: No default outlook.
629+ type: string
630+ username:
631+ default: admin001
632+ description: The name of the initial account (given admin permissions).
633+ type: string
634+ skill-level:
635+ description: A number indicating skill.
636+ type: int
637+ agility-ratio:
638+ description: A number from 0 to 1 indicating agility.
639+ type: float
640+`
641+
642+func repoConfig(name string) io.Reader {
643+ file, err := os.Open(filepath.Join("testrepo", "series", name, "config.yaml"))
644+ if err != nil {
645+ panic(err)
646+ }
647+ defer file.Close()
648+ data, err := ioutil.ReadAll(file)
649+ if err != nil {
650+ panic(err)
651+ }
652+ return bytes.NewBuffer(data)
653+}
654+
655+func (s *S) TestReadConfig(c *C) {
656+ config, err := charm.ReadConfig(repoConfig("dummy"))
657+ c.Assert(err, IsNil)
658+ c.Assert(config.Options["title"], Equals,
659+ charm.Option{
660+ Default: "My Title",
661+ Description: "A descriptive title used for the service.",
662+ Type: "string",
663+ },
664+ )
665+}
666+
667+func (s *S) TestConfigError(c *C) {
668+ _, err := charm.ReadConfig(bytes.NewBuffer([]byte(`options: {t: {type: foo}}`)))
669+ c.Assert(err, ErrorMatches, `config: options.t.type: unsupported value`)
670+}
671+
672+func (s *S) TestParseSample(c *C) {
673+ config, err := charm.ReadConfig(bytes.NewBuffer([]byte(sampleConfig)))
674+ c.Assert(err, IsNil)
675+
676+ opt := config.Options
677+ c.Assert(opt["title"], Equals,
678+ charm.Option{
679+ Default: "My Title",
680+ Description: "A descriptive title used for the service.",
681+ Type: "string",
682+ },
683+ )
684+ c.Assert(opt["outlook"], Equals,
685+ charm.Option{
686+ Description: "No default outlook.",
687+ Type: "string",
688+ },
689+ )
690+ c.Assert(opt["username"], Equals,
691+ charm.Option{
692+ Default: "admin001",
693+ Description: "The name of the initial account (given admin permissions).",
694+ Type: "string",
695+ },
696+ )
697+ c.Assert(opt["skill-level"], Equals,
698+ charm.Option{
699+ Description: "A number indicating skill.",
700+ Type: "int",
701+ },
702+ )
703+}
704+
705+func (s *S) TestValidate(c *C) {
706+ config, err := charm.ReadConfig(bytes.NewBuffer([]byte(sampleConfig)))
707+ c.Assert(err, IsNil)
708+
709+ input := map[string]string{
710+ "title": "Helpful Title",
711+ "outlook": "Peachy",
712+ }
713+
714+ // This should include an overridden value, a default and a new value.
715+ expected := map[string]interface{}{
716+ "title": "Helpful Title",
717+ "outlook": "Peachy",
718+ "username": "admin001",
719+ }
720+
721+ output, err := config.Validate(input)
722+ c.Assert(err, IsNil)
723+ c.Assert(output, Equals, expected)
724+
725+ // Check whether float conversion is working.
726+ input["agility-ratio"] = "0.5"
727+ input["skill-level"] = "7"
728+ expected["agility-ratio"] = 0.5
729+ expected["skill-level"] = int64(7)
730+ output, err = config.Validate(input)
731+ c.Assert(err, IsNil)
732+ c.Assert(output, Equals, expected)
733+
734+ // Check whether float errors are caught.
735+ input["agility-ratio"] = "foo"
736+ output, err = config.Validate(input)
737+ c.Assert(err, ErrorMatches, `Value for "agility-ratio" is not a float: "foo"`)
738+ input["agility-ratio"] = "0.5"
739+
740+ // Check whether int errors are caught.
741+ input["skill-level"] = "foo"
742+ output, err = config.Validate(input)
743+ c.Assert(err, ErrorMatches, `Value for "skill-level" is not an int: "foo"`)
744+ input["skill-level"] = "7"
745+
746+ // Now try to set a value outside the expected.
747+ input["bad"] = "value"
748+ output, err = config.Validate(input)
749+ c.Assert(output, IsNil)
750+ c.Assert(err, ErrorMatches, `Unknown configuration option: "bad"`)
751+}
752
753=== added file 'charm/dir.go'
754--- charm/dir.go 1970-01-01 00:00:00 +0000
755+++ charm/dir.go 2012-01-18 17:30:30 +0000
756@@ -0,0 +1,174 @@
757+package charm
758+
759+import (
760+ "archive/zip"
761+ "errors"
762+ "fmt"
763+ "io"
764+ "io/ioutil"
765+ "os"
766+ "path/filepath"
767+ "strconv"
768+ "syscall"
769+)
770+
771+// The Dir type encapsulates access to data and operations
772+// on a charm directory.
773+type Dir struct {
774+ Path string
775+ meta *Meta
776+ config *Config
777+ revision int
778+}
779+
780+// Trick to ensure *Dir implements the Charm interface.
781+var _ Charm = (*Dir)(nil)
782+
783+// ReadDir returns a Dir representing an expanded charm directory.
784+func ReadDir(path string) (dir *Dir, err error) {
785+ dir = &Dir{Path: path}
786+ file, err := os.Open(dir.join("metadata.yaml"))
787+ if err != nil {
788+ return nil, err
789+ }
790+ dir.meta, err = ReadMeta(file)
791+ file.Close()
792+ if err != nil {
793+ return nil, err
794+ }
795+ file, err = os.Open(dir.join("config.yaml"))
796+ if err != nil {
797+ return nil, err
798+ }
799+ dir.config, err = ReadConfig(file)
800+ file.Close()
801+ if err != nil {
802+ return nil, err
803+ }
804+ if file, err = os.Open(dir.join("revision")); err == nil {
805+ _, err = fmt.Fscan(file, &dir.revision)
806+ file.Close()
807+ if err != nil {
808+ return nil, errors.New("invalid revision file")
809+ }
810+ } else {
811+ dir.revision = dir.meta.OldRevision
812+ }
813+ return dir, nil
814+}
815+
816+// join builds a path rooted at the charm's expanded directory
817+// path and the extra path components provided.
818+func (dir *Dir) join(parts ...string) string {
819+ parts = append([]string{dir.Path}, parts...)
820+ return filepath.Join(parts...)
821+}
822+
823+// Revision returns the revision number for the charm
824+// expanded in dir.
825+func (dir *Dir) Revision() int {
826+ return dir.revision
827+}
828+
829+// Meta returns the Meta representing the metadata.yaml file
830+// for the charm expanded in dir.
831+func (dir *Dir) Meta() *Meta {
832+ return dir.meta
833+}
834+
835+// Config returns the Config representing the config.yaml file
836+// for the charm expanded in dir.
837+func (dir *Dir) Config() *Config {
838+ return dir.config
839+}
840+
841+// SetRevision changes the charm revision number. This affects
842+// the revision reported by Revision and the revision of the
843+// charm bundled by BundleTo.
844+// The revision file in the charm directory is not modified.
845+func (dir *Dir) SetRevision(revision int) {
846+ dir.revision = revision
847+}
848+
849+// SetDiskRevision does the same as SetRevision but also changes
850+// the revision file in the charm directory.
851+func (dir *Dir) SetDiskRevision(revision int) error {
852+ dir.SetRevision(revision)
853+ file, err := os.OpenFile(dir.join("revision"), os.O_WRONLY|os.O_CREATE, 0644)
854+ if err != nil {
855+ return err
856+ }
857+ _, err = file.Write([]byte(strconv.Itoa(revision)))
858+ file.Close()
859+ return err
860+}
861+
862+// BundleTo creates a charm file from the charm expanded in dir.
863+func (dir *Dir) BundleTo(w io.Writer) (err error) {
864+ zipw := zip.NewWriter(w)
865+ defer zipw.Close()
866+ zp := zipPacker{zipw, dir.Path}
867+ zp.AddRevision(dir.revision)
868+ return filepath.Walk(dir.Path, zp.WalkFunc())
869+}
870+
871+type zipPacker struct {
872+ *zip.Writer
873+ root string
874+}
875+
876+func (zp *zipPacker) WalkFunc() filepath.WalkFunc {
877+ return func(path string, fi os.FileInfo, err error) error {
878+ return zp.visit(path, fi, err)
879+ }
880+}
881+
882+func (zp *zipPacker) AddRevision(revision int) error {
883+ h := &zip.FileHeader{Name: "revision"}
884+ h.SetMode(syscall.S_IFREG | 0644)
885+ w, err := zp.CreateHeader(h)
886+ if err == nil {
887+ _, err = w.Write([]byte(strconv.Itoa(revision)))
888+ }
889+ return err
890+}
891+
892+func (zp *zipPacker) visit(path string, fi os.FileInfo, err error) error {
893+ if err != nil {
894+ return err
895+ }
896+ relpath, err := filepath.Rel(zp.root, path)
897+ if err != nil {
898+ return err
899+ }
900+ method := zip.Deflate
901+ hidden := len(relpath) > 1 && relpath[0] == '.'
902+ if fi.IsDir() {
903+ if relpath == "build" {
904+ return filepath.SkipDir
905+ }
906+ if hidden {
907+ return filepath.SkipDir
908+ }
909+ relpath += "/"
910+ method = zip.Store
911+ }
912+ if hidden || relpath == "revision" {
913+ return nil
914+ }
915+ h := &zip.FileHeader{
916+ Name: relpath,
917+ Method: method,
918+ }
919+ h.SetMode(fi.Mode())
920+ w, err := zp.CreateHeader(h)
921+ if err != nil || fi.IsDir() {
922+ return err
923+ }
924+ data, err := ioutil.ReadFile(path)
925+ if err != nil {
926+ return err
927+ }
928+ _, err = w.Write(data)
929+ return err
930+}
931
932=== added file 'charm/dir_test.go'
933--- charm/dir_test.go 1970-01-01 00:00:00 +0000
934+++ charm/dir_test.go 2012-01-18 17:30:30 +0000
935@@ -0,0 +1,167 @@
936+package charm_test
937+
938+import (
939+ "archive/zip"
940+ "bytes"
941+ "io/ioutil"
942+ . "launchpad.net/gocheck"
943+ "launchpad.net/juju/go/charm"
944+ "os"
945+ "path/filepath"
946+)
947+
948+func repoDir(name string) (path string) {
949+ return filepath.Join("testrepo", "series", name)
950+}
951+
952+func (s *S) TestReadDir(c *C) {
953+ path := repoDir("dummy")
954+ dir, err := charm.ReadDir(path)
955+ c.Assert(err, IsNil)
956+ checkDummy(c, dir, path)
957+}
958+
959+func (s *S) TestBundleTo(c *C) {
960+ dir, err := charm.ReadDir(repoDir("dummy"))
961+ c.Assert(err, IsNil)
962+
963+ path := filepath.Join(c.MkDir(), "bundle.charm")
964+ file, err := os.Create(path)
965+ c.Assert(err, IsNil)
966+ err = dir.BundleTo(file)
967+ file.Close()
968+ c.Assert(err, IsNil)
969+
970+ zipr, err := zip.OpenReader(path)
971+ c.Assert(err, IsNil)
972+ defer zipr.Close()
973+
974+ var metaf, instf, emptyf, revf *zip.File
975+ for _, f := range zipr.File {
976+ c.Logf("Bundled file: %s", f.Name)
977+ switch f.Name {
978+ case "revision":
979+ revf = f
980+ case "metadata.yaml":
981+ metaf = f
982+ case "hooks/install":
983+ instf = f
984+ case "empty/":
985+ emptyf = f
986+ case "build/ignored":
987+ c.Errorf("bundle includes build/*: %s", f.Name)
988+ case ".ignored", ".dir/ignored":
989+ c.Errorf("bundle includes .* entries: %s", f.Name)
990+ }
991+ }
992+
993+ c.Assert(revf, NotNil)
994+ reader, err := revf.Open()
995+ c.Assert(err, IsNil)
996+ data, err := ioutil.ReadAll(reader)
997+ reader.Close()
998+ c.Assert(err, IsNil)
999+ c.Assert(string(data), Equals, "1")
1000+
1001+ c.Assert(metaf, NotNil)
1002+ reader, err = metaf.Open()
1003+ c.Assert(err, IsNil)
1004+ meta, err := charm.ReadMeta(reader)
1005+ reader.Close()
1006+ c.Assert(err, IsNil)
1007+ c.Assert(meta.Name, Equals, "dummy")
1008+
1009+ c.Assert(instf, NotNil)
1010+ mode, err := instf.Mode()
1011+ c.Assert(err, IsNil)
1012+ c.Assert(mode&0700, Equals, os.FileMode(0700))
1013+
1014+ c.Assert(emptyf, NotNil)
1015+ mode, err = emptyf.Mode()
1016+ c.Assert(err, IsNil)
1017+ c.Assert(mode&os.ModeType, Equals, os.ModeDir)
1018+}
1019+
1020+func copyCharmDir(dst, src string) {
1021+ dir, err := charm.ReadDir(src)
1022+ if err != nil {
1023+ panic(err)
1024+ }
1025+ var b bytes.Buffer
1026+ err = dir.BundleTo(&b)
1027+ if err != nil {
1028+ panic(err)
1029+ }
1030+ bundle, err := charm.ReadBundleBytes(b.Bytes())
1031+ if err != nil {
1032+ panic(err)
1033+ }
1034+ err = bundle.ExpandTo(dst)
1035+ if err != nil {
1036+ panic(err)
1037+ }
1038+}
1039+
1040+func (s *S) TestDirRevisionFile(c *C) {
1041+ charmDir := c.MkDir()
1042+ copyCharmDir(charmDir, repoDir("dummy"))
1043+ revPath := filepath.Join(charmDir, "revision")
1044+
1045+ // Missing revision file
1046+ err := os.Remove(revPath)
1047+ c.Assert(err, IsNil)
1048+
1049+ dir, err := charm.ReadDir(charmDir)
1050+ c.Assert(err, IsNil)
1051+ c.Assert(dir.Revision(), Equals, 0)
1052+
1053+ // Missing revision file with old revision in metadata
1054+ file, err := os.OpenFile(filepath.Join(charmDir, "metadata.yaml"), os.O_WRONLY|os.O_APPEND, 0)
1055+ c.Assert(err, IsNil)
1056+ _, err = file.Write([]byte("\nrevision: 1234\n"))
1057+ c.Assert(err, IsNil)
1058+
1059+ dir, err = charm.ReadDir(charmDir)
1060+ c.Assert(err, IsNil)
1061+ c.Assert(dir.Revision(), Equals, 1234)
1062+
1063+ // Revision file with bad content
1064+ err = ioutil.WriteFile(revPath, []byte("garbage"), 0666)
1065+ c.Assert(err, IsNil)
1066+
1067+ dir, err = charm.ReadDir(charmDir)
1068+ c.Assert(err, ErrorMatches, "invalid revision file")
1069+ c.Assert(dir, IsNil)
1070+}
1071+
1072+func (s *S) TestDirSetRevision(c *C) {
1073+ dir, err := charm.ReadDir(repoDir("dummy"))
1074+ c.Assert(err, IsNil)
1075+
1076+ c.Assert(dir.Revision(), Equals, 1)
1077+ dir.SetRevision(42)
1078+ c.Assert(dir.Revision(), Equals, 42)
1079+
1080+ var b bytes.Buffer
1081+ err = dir.BundleTo(&b)
1082+ c.Assert(err, IsNil)
1083+
1084+ bundle, err := charm.ReadBundleBytes(b.Bytes())
1085+ c.Assert(bundle.Revision(), Equals, 42)
1086+}
1087+
1088+func (s *S) TestDirSetDiskRevision(c *C) {
1089+ charmDir := c.MkDir()
1090+ copyCharmDir(charmDir, repoDir("dummy"))
1091+
1092+ dir, err := charm.ReadDir(charmDir)
1093+ c.Assert(err, IsNil)
1094+
1095+ c.Assert(dir.Revision(), Equals, 1)
1096+ dir.SetDiskRevision(42)
1097+ c.Assert(dir.Revision(), Equals, 42)
1098+
1099+ dir, err = charm.ReadDir(charmDir)
1100+ c.Assert(err, IsNil)
1101+ c.Assert(dir.Revision(), Equals, 42)
1102+}
1103
1104=== added file 'charm/export_test.go'
1105--- charm/export_test.go 1970-01-01 00:00:00 +0000
1106+++ charm/export_test.go 2012-01-18 17:30:30 +0000
1107@@ -0,0 +1,11 @@
1108+package charm
1109+
1110+import (
1111+ "launchpad.net/juju/go/schema"
1112+)
1113+
1114+// Export meaningful bits for tests only.
1115+
1116+func IfaceExpander(limit interface{}) schema.Checker {
1117+ return ifaceExpander(limit)
1118+}
1119
1120=== added file 'charm/meta.go'
1121--- charm/meta.go 1970-01-01 00:00:00 +0000
1122+++ charm/meta.go 2012-01-18 17:30:30 +0000
1123@@ -0,0 +1,163 @@
1124+package charm
1125+
1126+import (
1127+ "errors"
1128+ "io"
1129+ "io/ioutil"
1130+ "launchpad.net/goyaml"
1131+ "launchpad.net/juju/go/schema"
1132+)
1133+
1134+// Relation represents a single relation defined in the charm
1135+// metadata.yaml file.
1136+type Relation struct {
1137+ Interface string
1138+ Optional bool
1139+ Limit int
1140+}
1141+
1142+// Meta represents all the known content that may be defined
1143+// within a charm's metadata.yaml file.
1144+type Meta struct {
1145+ Name string
1146+ Summary string
1147+ Description string
1148+ Provides map[string]Relation
1149+ Requires map[string]Relation
1150+ Peers map[string]Relation
1151+ OldRevision int // Obsolete
1152+}
1153+
1154+// ReadMeta reads the content of a metadata.yaml file and returns
1155+// its representation.
1156+func ReadMeta(r io.Reader) (meta *Meta, err error) {
1157+ data, err := ioutil.ReadAll(r)
1158+ if err != nil {
1159+ return
1160+ }
1161+ raw := make(map[interface{}]interface{})
1162+ err = goyaml.Unmarshal(data, raw)
1163+ if err != nil {
1164+ return
1165+ }
1166+ v, err := charmSchema.Coerce(raw, nil)
1167+ if err != nil {
1168+ return nil, errors.New("metadata: " + err.Error())
1169+ }
1170+ m := v.(schema.MapType)
1171+ meta = &Meta{}
1172+ meta.Name = m["name"].(string)
1173+ // Schema decodes as int64, but the int range should be good
1174+ // enough for revisions.
1175+ meta.Summary = m["summary"].(string)
1176+ meta.Description = m["description"].(string)
1177+ meta.Provides = parseRelations(m["provides"])
1178+ meta.Requires = parseRelations(m["requires"])
1179+ meta.Peers = parseRelations(m["peers"])
1180+ if rev := m["revision"]; rev != nil {
1181+ // Obsolete
1182+ meta.OldRevision = int(m["revision"].(int64))
1183+ }
1184+ return
1185+}
1186+
1187+func parseRelations(relations interface{}) map[string]Relation {
1188+ if relations == nil {
1189+ return nil
1190+ }
1191+ result := make(map[string]Relation)
1192+ for name, rel := range relations.(schema.MapType) {
1193+ relMap := rel.(schema.MapType)
1194+ relation := Relation{}
1195+ relation.Interface = relMap["interface"].(string)
1196+ relation.Optional = relMap["optional"].(bool)
1197+ if relMap["limit"] != nil {
1198+ // Schema defaults to int64, but we know
1199+ // the int range should be more than enough.
1200+ relation.Limit = int(relMap["limit"].(int64))
1201+ }
1202+ result[name.(string)] = relation
1203+ }
1204+ return result
1205+}
1206+
1207+// Schema coercer that expands the interface shorthand notation.
1208+// A consistent format is easier to work with than considering the
1209+// potential difference everywhere.
1210+//
1211+// Supports the following variants::
1212+//
1213+// provides:
1214+// server: riak
1215+// admin: http
1216+// foobar:
1217+// interface: blah
1218+//
1219+// provides:
1220+// server:
1221+// interface: mysql
1222+// limit:
1223+// optional: false
1224+//
1225+// In all input cases, the output is the fully specified interface
1226+// representation as seen in the mysql interface description above.
1227+func ifaceExpander(limit interface{}) schema.Checker {
1228+ return ifaceExpC{limit}
1229+}
1230+
1231+type ifaceExpC struct {
1232+ limit interface{}
1233+}
1234+
1235+var (
1236+ stringC = schema.String()
1237+ mapC = schema.Map(schema.String(), schema.Any())
1238+)
1239+
1240+func (c ifaceExpC) Coerce(v interface{}, path []string) (newv interface{}, err error) {
1241+ s, err := stringC.Coerce(v, path)
1242+ if err == nil {
1243+ newv = schema.MapType{
1244+ "interface": s,
1245+ "limit": c.limit,
1246+ "optional": false,
1247+ }
1248+ return
1249+ }
1250+
1251+ // Optional values are context-sensitive and/or have
1252+ // defaults, which is different than what KeyDict can
1253+ // readily support. So just do it here first, then
1254+ // coerce to the real schema.
1255+ v, err = mapC.Coerce(v, path)
1256+ if err != nil {
1257+ return
1258+ }
1259+ m := v.(schema.MapType)
1260+ if _, ok := m["limit"]; !ok {
1261+ m["limit"] = c.limit
1262+ }
1263+ if _, ok := m["optional"]; !ok {
1264+ m["optional"] = false
1265+ }
1266+ return ifaceSchema.Coerce(m, path)
1267+}
1268+
1269+var ifaceSchema = schema.FieldMap(schema.Fields{
1270+ "interface": schema.String(),
1271+ "limit": schema.OneOf(schema.Const(nil), schema.Int()),
1272+ "optional": schema.Bool(),
1273+}, nil)
1274+
1275+var charmSchema = schema.FieldMap(
1276+ schema.Fields{
1277+ "name": schema.String(),
1278+ "summary": schema.String(),
1279+ "description": schema.String(),
1280+ "peers": schema.Map(schema.String(), ifaceExpander(1)),
1281+ "provides": schema.Map(schema.String(), ifaceExpander(nil)),
1282+ "requires": schema.Map(schema.String(), ifaceExpander(1)),
1283+ "revision": schema.Int(), // Obsolete
1284+ },
1285+ schema.Optional{"provides", "requires", "peers", "revision"},
1286+)
1287
1288=== added file 'charm/meta_test.go'
1289--- charm/meta_test.go 1970-01-01 00:00:00 +0000
1290+++ charm/meta_test.go 2012-01-18 17:30:30 +0000
1291@@ -0,0 +1,108 @@
1292+package charm_test
1293+
1294+import (
1295+ "bytes"
1296+ "io"
1297+ "io/ioutil"
1298+ . "launchpad.net/gocheck"
1299+ "launchpad.net/juju/go/charm"
1300+ "launchpad.net/juju/go/schema"
1301+ "os"
1302+ "path/filepath"
1303+)
1304+
1305+func repoMeta(name string) io.Reader {
1306+ file, err := os.Open(filepath.Join("testrepo", "series", name, "metadata.yaml"))
1307+ if err != nil {
1308+ panic(err)
1309+ }
1310+ defer file.Close()
1311+ data, err := ioutil.ReadAll(file)
1312+ if err != nil {
1313+ panic(err)
1314+ }
1315+ return bytes.NewBuffer(data)
1316+}
1317+
1318+func (s *S) TestReadMeta(c *C) {
1319+ meta, err := charm.ReadMeta(repoMeta("dummy"))
1320+ c.Assert(err, IsNil)
1321+ c.Assert(meta.Name, Equals, "dummy")
1322+ c.Assert(meta.Summary, Equals, "That's a dummy charm.")
1323+ c.Assert(meta.Description, Equals,
1324+ "This is a longer description which\npotentially contains multiple lines.\n")
1325+ c.Assert(meta.OldRevision, Equals, 0)
1326+}
1327+
1328+func (s *S) TestParseMetaRelations(c *C) {
1329+ meta, err := charm.ReadMeta(repoMeta("mysql"))
1330+ c.Assert(err, IsNil)
1331+ c.Assert(meta.Provides["server"], Equals, charm.Relation{Interface: "mysql"})
1332+ c.Assert(meta.Requires, IsNil)
1333+ c.Assert(meta.Peers, IsNil)
1334+
1335+ meta, err = charm.ReadMeta(repoMeta("riak"))
1336+ c.Assert(err, IsNil)
1337+ c.Assert(meta.Provides["endpoint"], Equals, charm.Relation{Interface: "http"})
1338+ c.Assert(meta.Provides["admin"], Equals, charm.Relation{Interface: "http"})
1339+ c.Assert(meta.Peers["ring"], Equals, charm.Relation{Interface: "riak", Limit: 1})
1340+ c.Assert(meta.Requires, IsNil)
1341+
1342+ meta, err = charm.ReadMeta(repoMeta("wordpress"))
1343+ c.Assert(err, IsNil)
1344+ c.Assert(meta.Provides["url"], Equals, charm.Relation{Interface: "http"})
1345+ c.Assert(meta.Requires["db"], Equals, charm.Relation{Interface: "mysql", Limit: 1})
1346+ c.Assert(meta.Requires["cache"], Equals, charm.Relation{Interface: "varnish", Limit: 2, Optional: true})
1347+ c.Assert(meta.Peers, IsNil)
1348+
1349+}
1350+
1351+// Test rewriting of a given interface specification into long form.
1352+//
1353+// InterfaceExpander uses `coerce` to do one of two things:
1354+//
1355+// - Rewrite shorthand to the long form used for actual storage
1356+// - Fills in defaults, including a configurable `limit`
1357+//
1358+// This test ensures test coverage on each of these branches, along
1359+// with ensuring the conversion object properly raises SchemaError
1360+// exceptions on invalid data.
1361+func (s *S) TestIfaceExpander(c *C) {
1362+ e := charm.IfaceExpander(nil)
1363+
1364+ path := []string{"<pa", "th>"}
1365+
1366+ // Shorthand is properly rewritten
1367+ v, err := e.Coerce("http", path)
1368+ c.Assert(err, IsNil)
1369+ c.Assert(v, Equals, schema.MapType{"interface": "http", "limit": nil, "optional": false})
1370+
1371+ // Defaults are properly applied
1372+ v, err = e.Coerce(schema.MapType{"interface": "http"}, path)
1373+ c.Assert(err, IsNil)
1374+ c.Assert(v, Equals, schema.MapType{"interface": "http", "limit": nil, "optional": false})
1375+
1376+ v, err = e.Coerce(schema.MapType{"interface": "http", "limit": 2}, path)
1377+ c.Assert(err, IsNil)
1378+ c.Assert(v, Equals, schema.MapType{"interface": "http", "limit": int64(2), "optional": false})
1379+
1380+ v, err = e.Coerce(schema.MapType{"interface": "http", "optional": true}, path)
1381+ c.Assert(err, IsNil)
1382+ c.Assert(v, Equals, schema.MapType{"interface": "http", "limit": nil, "optional": true})
1383+
1384+ // Invalid data raises an error.
1385+ v, err = e.Coerce(42, path)
1386+ c.Assert(err, ErrorMatches, "<path>: expected map, got 42")
1387+
1388+ v, err = e.Coerce(schema.MapType{"interface": "http", "optional": nil}, path)
1389+ c.Assert(err, ErrorMatches, "<path>.optional: expected bool, got nothing")
1390+
1391+ v, err = e.Coerce(schema.MapType{"interface": "http", "limit": "none, really"}, path)
1392+ c.Assert(err, ErrorMatches, "<path>.limit: unsupported value")
1393+
1394+ // Can change default limit
1395+ e = charm.IfaceExpander(1)
1396+ v, err = e.Coerce(schema.MapType{"interface": "http"}, path)
1397+ c.Assert(err, IsNil)
1398+ c.Assert(v, Equals, schema.MapType{"interface": "http", "limit": int64(1), "optional": false})
1399+}
1400
1401=== added directory 'charm/testrepo'
1402=== added directory 'charm/testrepo/series'
1403=== added directory 'charm/testrepo/series/dummy'
1404=== added directory 'charm/testrepo/series/dummy/.dir'
1405=== added file 'charm/testrepo/series/dummy/.dir/ignored'
1406=== added file 'charm/testrepo/series/dummy/.ignored'
1407--- charm/testrepo/series/dummy/.ignored 1970-01-01 00:00:00 +0000
1408+++ charm/testrepo/series/dummy/.ignored 2012-01-18 17:30:30 +0000
1409@@ -0,0 +1,1 @@
1410+#
1411\ No newline at end of file
1412
1413=== added directory 'charm/testrepo/series/dummy/build'
1414=== added file 'charm/testrepo/series/dummy/build/ignored'
1415=== added file 'charm/testrepo/series/dummy/config.yaml'
1416--- charm/testrepo/series/dummy/config.yaml 1970-01-01 00:00:00 +0000
1417+++ charm/testrepo/series/dummy/config.yaml 2012-01-18 17:30:30 +0000
1418@@ -0,0 +1,5 @@
1419+options:
1420+ title: {default: My Title, description: A descriptive title used for the service., type: string}
1421+ outlook: {description: No default outlook., type: string}
1422+ username: {default: admin001, description: The name of the initial account (given admin permissions)., type: string}
1423+ skill-level: {description: A number indicating skill., type: int}
1424
1425=== added directory 'charm/testrepo/series/dummy/empty'
1426=== added directory 'charm/testrepo/series/dummy/hooks'
1427=== added file 'charm/testrepo/series/dummy/hooks/install'
1428--- charm/testrepo/series/dummy/hooks/install 1970-01-01 00:00:00 +0000
1429+++ charm/testrepo/series/dummy/hooks/install 2012-01-18 17:30:30 +0000
1430@@ -0,0 +1,2 @@
1431+#!/bin/bash
1432+echo "Done!"
1433
1434=== added file 'charm/testrepo/series/dummy/metadata.yaml'
1435--- charm/testrepo/series/dummy/metadata.yaml 1970-01-01 00:00:00 +0000
1436+++ charm/testrepo/series/dummy/metadata.yaml 2012-01-18 17:30:30 +0000
1437@@ -0,0 +1,5 @@
1438+name: dummy
1439+summary: "That's a dummy charm."
1440+description: |
1441+ This is a longer description which
1442+ potentially contains multiple lines.
1443
1444=== added file 'charm/testrepo/series/dummy/revision'
1445--- charm/testrepo/series/dummy/revision 1970-01-01 00:00:00 +0000
1446+++ charm/testrepo/series/dummy/revision 2012-01-18 17:30:30 +0000
1447@@ -0,0 +1,1 @@
1448+1
1449\ No newline at end of file
1450
1451=== added directory 'charm/testrepo/series/dummy/src'
1452=== added file 'charm/testrepo/series/dummy/src/hello.c'
1453--- charm/testrepo/series/dummy/src/hello.c 1970-01-01 00:00:00 +0000
1454+++ charm/testrepo/series/dummy/src/hello.c 2012-01-18 17:30:30 +0000
1455@@ -0,0 +1,7 @@
1456+#include <stdio.h>
1457+
1458+main()
1459+{
1460+ printf ("Hello World!\n");
1461+ return 0;
1462+}
1463
1464=== added directory 'charm/testrepo/series/mysql'
1465=== added directory 'charm/testrepo/series/mysql-alternative'
1466=== added file 'charm/testrepo/series/mysql-alternative/metadata.yaml'
1467--- charm/testrepo/series/mysql-alternative/metadata.yaml 1970-01-01 00:00:00 +0000
1468+++ charm/testrepo/series/mysql-alternative/metadata.yaml 2012-01-18 17:30:30 +0000
1469@@ -0,0 +1,9 @@
1470+name: mysql-alternative
1471+summary: "Database engine"
1472+description: "A pretty popular database"
1473+provides:
1474+ prod:
1475+ interface: mysql
1476+ dev:
1477+ interface: mysql
1478+ limit: 2
1479
1480=== added file 'charm/testrepo/series/mysql-alternative/revision'
1481--- charm/testrepo/series/mysql-alternative/revision 1970-01-01 00:00:00 +0000
1482+++ charm/testrepo/series/mysql-alternative/revision 2012-01-18 17:30:30 +0000
1483@@ -0,0 +1,1 @@
1484+1
1485\ No newline at end of file
1486
1487=== added file 'charm/testrepo/series/mysql/metadata.yaml'
1488--- charm/testrepo/series/mysql/metadata.yaml 1970-01-01 00:00:00 +0000
1489+++ charm/testrepo/series/mysql/metadata.yaml 2012-01-18 17:30:30 +0000
1490@@ -0,0 +1,5 @@
1491+name: mysql
1492+summary: "Database engine"
1493+description: "A pretty popular database"
1494+provides:
1495+ server: mysql
1496
1497=== added file 'charm/testrepo/series/mysql/revision'
1498--- charm/testrepo/series/mysql/revision 1970-01-01 00:00:00 +0000
1499+++ charm/testrepo/series/mysql/revision 2012-01-18 17:30:30 +0000
1500@@ -0,0 +1,1 @@
1501+1
1502\ No newline at end of file
1503
1504=== added directory 'charm/testrepo/series/new'
1505=== added file 'charm/testrepo/series/new/metadata.yaml'
1506--- charm/testrepo/series/new/metadata.yaml 1970-01-01 00:00:00 +0000
1507+++ charm/testrepo/series/new/metadata.yaml 2012-01-18 17:30:30 +0000
1508@@ -0,0 +1,5 @@
1509+name: sample
1510+summary: "That's a sample charm."
1511+description: |
1512+ This is a longer description which
1513+ potentially contains multiple lines.
1514
1515=== added file 'charm/testrepo/series/new/revision'
1516--- charm/testrepo/series/new/revision 1970-01-01 00:00:00 +0000
1517+++ charm/testrepo/series/new/revision 2012-01-18 17:30:30 +0000
1518@@ -0,0 +1,1 @@
1519+2
1520
1521=== added directory 'charm/testrepo/series/old'
1522=== added file 'charm/testrepo/series/old/metadata.yaml'
1523--- charm/testrepo/series/old/metadata.yaml 1970-01-01 00:00:00 +0000
1524+++ charm/testrepo/series/old/metadata.yaml 2012-01-18 17:30:30 +0000
1525@@ -0,0 +1,5 @@
1526+name: sample
1527+summary: "That's a sample charm."
1528+description: |
1529+ This is a longer description which
1530+ potentially contains multiple lines.
1531
1532=== added file 'charm/testrepo/series/old/revision'
1533--- charm/testrepo/series/old/revision 1970-01-01 00:00:00 +0000
1534+++ charm/testrepo/series/old/revision 2012-01-18 17:30:30 +0000
1535@@ -0,0 +1,1 @@
1536+1
1537
1538=== added directory 'charm/testrepo/series/riak'
1539=== added file 'charm/testrepo/series/riak/metadata.yaml'
1540--- charm/testrepo/series/riak/metadata.yaml 1970-01-01 00:00:00 +0000
1541+++ charm/testrepo/series/riak/metadata.yaml 2012-01-18 17:30:30 +0000
1542@@ -0,0 +1,11 @@
1543+name: riak
1544+summary: "K/V storage engine"
1545+description: "Scalable K/V Store in Erlang with Clocks :-)"
1546+provides:
1547+ endpoint:
1548+ interface: http
1549+ admin:
1550+ interface: http
1551+peers:
1552+ ring:
1553+ interface: riak
1554
1555=== added file 'charm/testrepo/series/riak/revision'
1556--- charm/testrepo/series/riak/revision 1970-01-01 00:00:00 +0000
1557+++ charm/testrepo/series/riak/revision 2012-01-18 17:30:30 +0000
1558@@ -0,0 +1,1 @@
1559+7
1560\ No newline at end of file
1561
1562=== added directory 'charm/testrepo/series/varnish'
1563=== added directory 'charm/testrepo/series/varnish-alternative'
1564=== added directory 'charm/testrepo/series/varnish-alternative/hooks'
1565=== added file 'charm/testrepo/series/varnish-alternative/hooks/install'
1566--- charm/testrepo/series/varnish-alternative/hooks/install 1970-01-01 00:00:00 +0000
1567+++ charm/testrepo/series/varnish-alternative/hooks/install 2012-01-18 17:30:30 +0000
1568@@ -0,0 +1,3 @@
1569+#!/bin/bash
1570+
1571+echo hello world
1572\ No newline at end of file
1573
1574=== added file 'charm/testrepo/series/varnish-alternative/metadata.yaml'
1575--- charm/testrepo/series/varnish-alternative/metadata.yaml 1970-01-01 00:00:00 +0000
1576+++ charm/testrepo/series/varnish-alternative/metadata.yaml 2012-01-18 17:30:30 +0000
1577@@ -0,0 +1,5 @@
1578+name: varnish-alternative
1579+summary: "Database engine"
1580+description: "Another popular database"
1581+provides:
1582+ webcache: varnish
1583
1584=== added file 'charm/testrepo/series/varnish-alternative/revision'
1585--- charm/testrepo/series/varnish-alternative/revision 1970-01-01 00:00:00 +0000
1586+++ charm/testrepo/series/varnish-alternative/revision 2012-01-18 17:30:30 +0000
1587@@ -0,0 +1,1 @@
1588+1
1589\ No newline at end of file
1590
1591=== added file 'charm/testrepo/series/varnish/metadata.yaml'
1592--- charm/testrepo/series/varnish/metadata.yaml 1970-01-01 00:00:00 +0000
1593+++ charm/testrepo/series/varnish/metadata.yaml 2012-01-18 17:30:30 +0000
1594@@ -0,0 +1,5 @@
1595+name: varnish
1596+summary: "Database engine"
1597+description: "Another popular database"
1598+provides:
1599+ webcache: varnish
1600
1601=== added file 'charm/testrepo/series/varnish/revision'
1602--- charm/testrepo/series/varnish/revision 1970-01-01 00:00:00 +0000
1603+++ charm/testrepo/series/varnish/revision 2012-01-18 17:30:30 +0000
1604@@ -0,0 +1,1 @@
1605+1
1606\ No newline at end of file
1607
1608=== added directory 'charm/testrepo/series/wordpress'
1609=== added file 'charm/testrepo/series/wordpress/config.yaml'
1610--- charm/testrepo/series/wordpress/config.yaml 1970-01-01 00:00:00 +0000
1611+++ charm/testrepo/series/wordpress/config.yaml 2012-01-18 17:30:30 +0000
1612@@ -0,0 +1,3 @@
1613+options:
1614+ blog-title: {default: My Title, description: A descriptive title used for the blog., type: string}
1615+
1616
1617=== added file 'charm/testrepo/series/wordpress/metadata.yaml'
1618--- charm/testrepo/series/wordpress/metadata.yaml 1970-01-01 00:00:00 +0000
1619+++ charm/testrepo/series/wordpress/metadata.yaml 2012-01-18 17:30:30 +0000
1620@@ -0,0 +1,19 @@
1621+name: wordpress
1622+summary: "Blog engine"
1623+description: "A pretty popular blog engine"
1624+provides:
1625+ url:
1626+ interface: http
1627+ limit:
1628+ optional: false
1629+requires:
1630+ db:
1631+ interface: mysql
1632+ limit: 1
1633+ optional: false
1634+ cache:
1635+ interface: varnish
1636+ limit: 2
1637+ optional: true
1638+
1639+
1640
1641=== added file 'charm/testrepo/series/wordpress/revision'
1642--- charm/testrepo/series/wordpress/revision 1970-01-01 00:00:00 +0000
1643+++ charm/testrepo/series/wordpress/revision 2012-01-18 17:30:30 +0000
1644@@ -0,0 +1,1 @@
1645+3
1646\ No newline at end of file
1647
1648=== added file 'charm/url.go'
1649--- charm/url.go 1970-01-01 00:00:00 +0000
1650+++ charm/url.go 2012-01-18 17:30:30 +0000
1651@@ -0,0 +1,132 @@
1652+package charm
1653+
1654+import (
1655+ "fmt"
1656+ "regexp"
1657+ "strconv"
1658+ "strings"
1659+)
1660+
1661+// A charm URL represents charm locations such as:
1662+//
1663+// cs:~joe/oneiric/wordpress
1664+// cs:oneiric/wordpress-42
1665+// local:oneiric/wordpress
1666+//
1667+type URL struct {
1668+ Name string
1669+ Revision int // -1 if unset, 0 is valid
1670+ Collection
1671+}
1672+
1673+// A charm Collection represents a namespace of charms. The
1674+// collection precedes the charm name in a charm URL.
1675+type Collection struct {
1676+ Schema string
1677+ User string
1678+ Series string
1679+}
1680+
1681+// WithRevision returns a *URL with the same Name and Collection of url,
1682+// but with Revision set to the revision parameter. If url already has
1683+// the requested revision, url itself is returned.
1684+func (url *URL) WithRevision(revision int) *URL {
1685+ if url.Revision == revision {
1686+ return url
1687+ }
1688+ urlCopy := *url
1689+ urlCopy.Revision = revision
1690+ return &urlCopy
1691+}
1692+
1693+var validUser = regexp.MustCompile("^[a-z0-9][a-zA-Z0-9+.-]+$")
1694+var validSeries = regexp.MustCompile("^[a-z]+([a-z-]+[a-z])?$")
1695+var validName = regexp.MustCompile("^[a-z][a-z0-9]*(-[a-z0-9]*[a-z][a-z0-9]*)*$")
1696+
1697+// MustParseURL works like ParseURL, but panics in case of errors.
1698+func MustParseURL(url string) *URL {
1699+ u, err := ParseURL(url)
1700+ if err != nil {
1701+ panic(err)
1702+ }
1703+ return u
1704+}
1705+
1706+// ParseURL parses the provided charm URL string into its respective
1707+// structure.
1708+func ParseURL(url string) (*URL, error) {
1709+ u := &URL{}
1710+ i := strings.Index(url, ":")
1711+ if i > 0 {
1712+ u.Schema = url[:i]
1713+ i++
1714+ }
1715+ // cs: or local:
1716+ if u.Schema != "cs" && u.Schema != "local" {
1717+ return nil, fmt.Errorf("charm URL has invalid schema: %q", url)
1718+ }
1719+ parts := strings.Split(url[i:], "/")
1720+ if len(parts) < 1 || len(parts) > 3 {
1721+ return nil, fmt.Errorf("charm URL has invalid form: %q", url)
1722+ }
1723+
1724+ // ~<username>
1725+ if strings.HasPrefix(parts[0], "~") {
1726+ if u.Schema == "local" {
1727+ return nil, fmt.Errorf("local charm URL with user name: %q", url)
1728+ }
1729+ u.User = parts[0][1:]
1730+ if !validUser.MatchString(u.User) {
1731+ return nil, fmt.Errorf("charm URL has invalid user name: %q", url)
1732+ }
1733+ parts = parts[1:]
1734+ }
1735+
1736+ // <series>
1737+ if len(parts) < 2 {
1738+ return nil, fmt.Errorf("charm URL without series: %q", url)
1739+ }
1740+ if len(parts) == 2 {
1741+ u.Series = parts[0]
1742+ if !validSeries.MatchString(u.Series) {
1743+ return nil, fmt.Errorf("charm URL has invalid series: %q", url)
1744+ }
1745+ parts = parts[1:]
1746+ }
1747+
1748+ // <name>[-<revision>]
1749+ u.Name = parts[0]
1750+ u.Revision = -1
1751+ for i := len(u.Name) - 1; i > 0; i-- {
1752+ c := u.Name[i]
1753+ if c >= '0' && c <= '9' {
1754+ continue
1755+ }
1756+ if c == '-' && i != len(u.Name)-1 {
1757+ var err error
1758+ u.Revision, err = strconv.Atoi(u.Name[i+1:])
1759+ if err != nil {
1760+ panic(err) // We just checked it was right.
1761+ }
1762+ u.Name = u.Name[:i]
1763+ }
1764+ break
1765+ }
1766+ if !validName.MatchString(u.Name) {
1767+ return nil, fmt.Errorf("charm URL has invalid charm name: %q", url)
1768+ }
1769+ return u, nil
1770+}
1771+
1772+func (u *URL) String() string {
1773+ if u.User != "" {
1774+ if u.Revision >= 0 {
1775+ return fmt.Sprintf("%s:~%s/%s/%s-%d", u.Schema, u.User, u.Series, u.Name, u.Revision)
1776+ }
1777+ return fmt.Sprintf("%s:~%s/%s/%s", u.Schema, u.User, u.Series, u.Name)
1778+ }
1779+ if u.Revision >= 0 {
1780+ return fmt.Sprintf("%s:%s/%s-%d", u.Schema, u.Series, u.Name, u.Revision)
1781+ }
1782+ return fmt.Sprintf("%s:%s/%s", u.Schema, u.Series, u.Name)
1783+}
1784
1785=== added file 'charm/url_test.go'
1786--- charm/url_test.go 1970-01-01 00:00:00 +0000
1787+++ charm/url_test.go 2012-01-18 17:30:30 +0000
1788@@ -0,0 +1,61 @@
1789+package charm_test
1790+
1791+import (
1792+ . "launchpad.net/gocheck"
1793+ "launchpad.net/juju/go/charm"
1794+)
1795+
1796+var urlTests = []struct {
1797+ s, err string
1798+ url *charm.URL
1799+}{
1800+ {"cs:~user/series/name", "", &charm.URL{"name", -1, charm.Collection{"cs", "user", "series"}}},
1801+ {"cs:~user/series/name-0", "", &charm.URL{"name", 0, charm.Collection{"cs", "user", "series"}}},
1802+ {"cs:series/name", "", &charm.URL{"name", -1, charm.Collection{"cs", "", "series"}}},
1803+ {"cs:series/name-42", "", &charm.URL{"name", 42, charm.Collection{"cs", "", "series"}}},
1804+ {"local:series/name-1", "", &charm.URL{"name", 1, charm.Collection{"local", "", "series"}}},
1805+ {"local:series/name", "", &charm.URL{"name", -1, charm.Collection{"local", "", "series"}}},
1806+ {"local:series/n0-0n-n0", "", &charm.URL{"n0-0n-n0", -1, charm.Collection{"local", "", "series"}}},
1807+
1808+ {"bs:~user/series/name-1", "charm URL has invalid schema: .*", nil},
1809+ {"cs:~1/series/name-1", "charm URL has invalid user name: .*", nil},
1810+ {"cs:~user/1/name-1", "charm URL has invalid series: .*", nil},
1811+ {"cs:~user/series/name-1-2", "charm URL has invalid charm name: .*", nil},
1812+ {"cs:~user/series/name-1-name-2", "charm URL has invalid charm name: .*", nil},
1813+ {"cs:~user/series/name--name-2", "charm URL has invalid charm name: .*", nil},
1814+ {"cs:~user/series/huh/name-1", "charm URL has invalid form: .*", nil},
1815+ {"cs:~user/name", "charm URL without series: .*", nil},
1816+ {"cs:name", "charm URL without series: .*", nil},
1817+ {"local:~user/series/name", "local charm URL with user name: .*", nil},
1818+ {"local:~user/name", "local charm URL with user name: .*", nil},
1819+ {"local:name", "charm URL without series: .*", nil},
1820+}
1821+
1822+func (s *S) TestParseURL(c *C) {
1823+ for _, t := range urlTests {
1824+ url, err := charm.ParseURL(t.s)
1825+ bug := Bug("ParseURL(%q)", t.s)
1826+ if t.err != "" {
1827+ c.Check(err.Error(), Matches, t.err, bug)
1828+ } else {
1829+ c.Check(url, Equals, t.url, bug)
1830+ c.Check(t.url.String(), Equals, t.s)
1831+ }
1832+ }
1833+}
1834+
1835+func (s *S) TestMustParseURL(c *C) {
1836+ url := charm.MustParseURL("cs:series/name")
1837+ c.Assert(url, Equals, &charm.URL{"name", -1, charm.Collection{"cs", "", "series"}})
1838+ f := func() { charm.MustParseURL("local:name") }
1839+ c.Assert(f, PanicMatches, "charm URL without series: .*")
1840+}
1841+
1842+func (s *S) TestWithRevision(c *C) {
1843+ url := charm.MustParseURL("cs:series/name")
1844+ other := url.WithRevision(1)
1845+ c.Assert(url, Equals, &charm.URL{"name", -1, charm.Collection{"cs", "", "series"}})
1846+ c.Assert(other, Equals, &charm.URL{"name", 1, charm.Collection{"cs", "", "series"}})
1847+
1848+ c.Assert(other.WithRevision(1) == other, Equals, true)
1849+}
1850
1851=== added directory 'cloudinit'
1852=== added file 'cloudinit/Makefile'
1853--- cloudinit/Makefile 1970-01-01 00:00:00 +0000
1854+++ cloudinit/Makefile 2012-01-18 17:30:30 +0000
1855@@ -0,0 +1,24 @@
1856+include $(GOROOT)/src/Make.inc
1857+
1858+all: package
1859+
1860+TARG=launchpad.net/juju/go/cloudinit
1861+
1862+GOFILES=\
1863+ cloudinit.go\
1864+ options.go\
1865+
1866+GOFMT=gofmt
1867+BADFMT:=$(shell $(GOFMT) -l $(GOFILES) $(CGOFILES) $(wildcard *_test.go))
1868+
1869+gofmt: $(BADFMT)
1870+ @for F in $(BADFMT); do $(GOFMT) -w $$F && echo $$F; done
1871+
1872+ifneq ($(BADFMT),)
1873+ifneq ($(MAKECMDGOALS),gofmt)
1874+$(warning WARNING: make gofmt: $(BADFMT))
1875+endif
1876+endif
1877+
1878+include $(GOROOT)/src/Make.pkg
1879+
1880
1881=== added file 'cloudinit/cloudinit.go'
1882--- cloudinit/cloudinit.go 1970-01-01 00:00:00 +0000
1883+++ cloudinit/cloudinit.go 2012-01-18 17:30:30 +0000
1884@@ -0,0 +1,66 @@
1885+// The cloudinit package implements a way of creating
1886+// a cloud-init configuration file.
1887+// See https://help.ubuntu.com/community/CloudInit.
1888+package cloudinit
1889+
1890+import (
1891+ yaml "launchpad.net/goyaml"
1892+)
1893+
1894+// Config represents a set of cloud-init configuration options.
1895+type Config struct {
1896+ attrs map[string]interface{}
1897+}
1898+
1899+// New returns a new Config with no options set.
1900+func New() *Config {
1901+ return &Config{make(map[string]interface{})}
1902+}
1903+
1904+// Render returns the cloud-init configuration as a YAML file.
1905+func (cfg *Config) Render() ([]byte, error) {
1906+ data, err := yaml.Marshal(cfg.attrs)
1907+ if err != nil {
1908+ return nil, err
1909+ }
1910+ return append([]byte("#cloud-config\n"), data...), nil
1911+}
1912+
1913+func (cfg *Config) set(opt string, yes bool, value interface{}) {
1914+ if yes {
1915+ cfg.attrs[opt] = value
1916+ } else {
1917+ delete(cfg.attrs, opt)
1918+ }
1919+}
1920+
1921+// source is Key, or KeyId and KeyServer
1922+type source struct {
1923+ Source string `yaml:"source"`
1924+ Key string `yaml:"key,omitempty"`
1925+ KeyId string `yaml:"keyid,omitempty"`
1926+ KeyServer string `yaml:"keyserver,omitempty"`
1927+}
1928+
1929+// command represents a shell command.
1930+type command struct {
1931+ literal string
1932+ args []string
1933+}
1934+
1935+// GetYAML implements yaml.Getter
1936+func (t *command) GetYAML() (tag string, value interface{}) {
1937+ if t.args != nil {
1938+ return "", t.args
1939+ }
1940+ return "", t.literal
1941+}
1942+
1943+type SSHKeyType string
1944+
1945+const (
1946+ RSAPrivate SSHKeyType = "rsa_private"
1947+ RSAPublic SSHKeyType = "rsa_public"
1948+ DSAPrivate SSHKeyType = "dsa_private"
1949+ DSAPublic SSHKeyType = "dsa_public"
1950+)
1951
1952=== added file 'cloudinit/cloudinit_test.go'
1953--- cloudinit/cloudinit_test.go 1970-01-01 00:00:00 +0000
1954+++ cloudinit/cloudinit_test.go 2012-01-18 17:30:30 +0000
1955@@ -0,0 +1,200 @@
1956+package cloudinit_test
1957+
1958+import (
1959+ "fmt"
1960+ . "launchpad.net/gocheck"
1961+ "launchpad.net/juju/go/cloudinit"
1962+ "testing"
1963+)
1964+
1965+// TODO integration tests, but how?
1966+
1967+type S struct{}
1968+
1969+var _ = Suite(S{})
1970+
1971+func Test1(t *testing.T) {
1972+ TestingT(t)
1973+}
1974+
1975+var ctests = []struct {
1976+ name string
1977+ expect string
1978+ setOption func(cfg *cloudinit.Config)
1979+}{
1980+ {
1981+ "User",
1982+ "user: me\n",
1983+ func(cfg *cloudinit.Config) {
1984+ cfg.SetUser("me")
1985+ },
1986+ },
1987+ {
1988+ "AptUpgrade",
1989+ "apt_upgrade: true\n",
1990+ func(cfg *cloudinit.Config) {
1991+ cfg.SetAptUpgrade(true)
1992+ },
1993+ },
1994+ {
1995+ "AptUpdate",
1996+ "apt_update: true\n",
1997+ func(cfg *cloudinit.Config) {
1998+ cfg.SetAptUpdate(true)
1999+ },
2000+ },
2001+ {
2002+ "AptMirror",
2003+ "apt_mirror: http://foo.com\n",
2004+ func(cfg *cloudinit.Config) {
2005+ cfg.SetAptMirror("http://foo.com")
2006+ },
2007+ },
2008+ {
2009+ "AptPreserveSourcesList",
2010+ "apt_mirror: true\n",
2011+ func(cfg *cloudinit.Config) {
2012+ cfg.SetAptPreserveSourcesList(true)
2013+ },
2014+ },
2015+ {
2016+ "DebconfSelections",
2017+ "debconf_selections: '# Force debconf priority to critical.\n\n debconf debconf/priority select critical\n\n'\n",
2018+ func(cfg *cloudinit.Config) {
2019+ cfg.SetDebconfSelections("# Force debconf priority to critical.\ndebconf debconf/priority select critical\n")
2020+ },
2021+ },
2022+ {
2023+ "DisableEC2Metadata",
2024+ "disable_ec2_metadata: true\n",
2025+ func(cfg *cloudinit.Config) {
2026+ cfg.SetDisableEC2Metadata(true)
2027+ },
2028+ },
2029+ {
2030+ "FinalMessage",
2031+ "final_message: goodbye\n",
2032+ func(cfg *cloudinit.Config) {
2033+ cfg.SetFinalMessage("goodbye")
2034+ },
2035+ },
2036+ {
2037+ "Locale",
2038+ "locale: en_us\n",
2039+ func(cfg *cloudinit.Config) {
2040+ cfg.SetLocale("en_us")
2041+ },
2042+ },
2043+ {
2044+ "DisableRoot",
2045+ "disable_root: false\n",
2046+ func(cfg *cloudinit.Config) {
2047+ cfg.SetDisableRoot(false)
2048+ },
2049+ },
2050+ {
2051+ "SSHAuthorizedKeys",
2052+ "ssh_authorized_keys:\n- key1\n- key2\n",
2053+ func(cfg *cloudinit.Config) {
2054+ cfg.AddSSHAuthorizedKey("key1")
2055+ cfg.AddSSHAuthorizedKey("key2")
2056+ },
2057+ },
2058+ {
2059+ "SSHKeys RSA",
2060+ "ssh_keys:\n rsa_private: key1data\n rsa_public: key2data\n",
2061+ func(cfg *cloudinit.Config) {
2062+ cfg.AddSSHKey(cloudinit.RSAPrivate, "key1data")
2063+ cfg.AddSSHKey(cloudinit.RSAPublic, "key2data")
2064+ },
2065+ },
2066+ {
2067+ "SSHKeys DSA",
2068+ "ssh_keys:\n dsa_public: key1data\n dsa_private: key2data\n",
2069+ func(cfg *cloudinit.Config) {
2070+ cfg.AddSSHKey(cloudinit.DSAPublic, "key1data")
2071+ cfg.AddSSHKey(cloudinit.DSAPrivate, "key2data")
2072+ },
2073+ },
2074+ {
2075+ "Output",
2076+ "output:\n all:\n - '>foo'\n - '|bar'\n",
2077+ func(cfg *cloudinit.Config) {
2078+ cfg.SetOutput("all", ">foo", "|bar")
2079+ },
2080+ },
2081+ {
2082+ "Output",
2083+ "output:\n all: '>foo'\n",
2084+ func(cfg *cloudinit.Config) {
2085+ cfg.SetOutput(cloudinit.OutAll, ">foo", "")
2086+ },
2087+ },
2088+ {
2089+ "AptSources",
2090+ "apt_sources:\n- source: keyName\n key: someKey\n",
2091+ func(cfg *cloudinit.Config) {
2092+ cfg.AddAptSource("keyName", "someKey")
2093+ },
2094+ },
2095+ {
2096+ "AptSources",
2097+ "apt_sources:\n- source: keyName\n keyid: someKey\n keyserver: foo.com\n",
2098+ func(cfg *cloudinit.Config) {
2099+ cfg.AddAptSourceWithKeyId("keyName", "someKey", "foo.com")
2100+ },
2101+ },
2102+ {
2103+ "Packages",
2104+ "packages:\n- juju\n- ubuntu\n",
2105+ func(cfg *cloudinit.Config) {
2106+ cfg.AddPackage("juju")
2107+ cfg.AddPackage("ubuntu")
2108+ },
2109+ },
2110+ {
2111+ "BootCmd",
2112+ "bootcmd:\n- ls > /dev\n- - ls\n - '>with space'\n",
2113+ func(cfg *cloudinit.Config) {
2114+ cfg.AddBootCmd("ls > /dev")
2115+ cfg.AddBootCmdArgs("ls", ">with space")
2116+ },
2117+ },
2118+ {
2119+ "Mounts",
2120+ "mounts:\n- - x\n - \"y\"\n- - z\n - w\n",
2121+ func(cfg *cloudinit.Config) {
2122+ cfg.AddMount("x", "y")
2123+ cfg.AddMount("z", "w")
2124+ },
2125+ },
2126+}
2127+
2128+const header = "#cloud-config\n"
2129+
2130+func (S) TestOutput(c *C) {
2131+ for _, t := range ctests {
2132+ cfg := cloudinit.New()
2133+ t.setOption(cfg)
2134+ data, err := cfg.Render()
2135+ c.Assert(err, IsNil)
2136+ c.Assert(data, NotNil)
2137+ c.Assert(string(data), Equals, header+t.expect, Bug("test %q output differs", t.name))
2138+ }
2139+}
2140+
2141+//#cloud-config
2142+//packages:
2143+//- juju
2144+//- ubuntu
2145+func ExampleConfig() {
2146+ cfg := cloudinit.New()
2147+ cfg.AddPackage("juju")
2148+ cfg.AddPackage("ubuntu")
2149+ data, err := cfg.Render()
2150+ if err != nil {
2151+ fmt.Printf("render error: %v", err)
2152+ return
2153+ }
2154+ fmt.Printf("%s", data)
2155+}
2156
2157=== added file 'cloudinit/options.go'
2158--- cloudinit/options.go 1970-01-01 00:00:00 +0000
2159+++ cloudinit/options.go 2012-01-18 17:30:30 +0000
2160@@ -0,0 +1,214 @@
2161+package cloudinit
2162+
2163+// SetUser sets the user name that will be used for some other options.
2164+// The user will be assumed to already exist in the machine image.
2165+// The default user is "ubuntu".
2166+func (cfg *Config) SetUser(user string) {
2167+ cfg.set("user", user != "", user)
2168+}
2169+
2170+// SetAptUpgrade sets whether cloud-init runs "apt-get upgrade"
2171+// on first boot.
2172+func (cfg *Config) SetAptUpgrade(yes bool) {
2173+ cfg.set("apt_upgrade", yes, yes)
2174+}
2175+
2176+// SetUpdate sets whether cloud-init runs "apt-get update"
2177+// on first boot.
2178+func (cfg *Config) SetAptUpdate(yes bool) {
2179+ cfg.set("apt_update", yes, yes)
2180+}
2181+
2182+// SetAptMirror sets the URL to be used as the apt
2183+// mirror site. If not set, the URL is selected based
2184+// on cloud metadata in EC2 - <region>.archive.ubuntu.com
2185+func (cfg *Config) SetAptMirror(url string) {
2186+ cfg.set("apt_mirror", url != "", url)
2187+}
2188+
2189+// SetAptPreserveSourcesList sets whether /etc/apt/sources.list
2190+// is overwritten by the mirror. If true, SetAptMirror above
2191+// will have no effect.
2192+func (cfg *Config) SetAptPreserveSourcesList(yes bool) {
2193+ cfg.set("apt_mirror", yes, yes)
2194+}
2195+
2196+// AddAptSource adds an apt source. The key holds the
2197+// public key of the source, in the form expected by apt-key(8).
2198+func (cfg *Config) AddAptSource(name, key string) {
2199+ src, _ := cfg.attrs["apt_sources"].([]*source)
2200+ cfg.attrs["apt_sources"] = append(src,
2201+ &source{
2202+ Source: name,
2203+ Key: key,
2204+ })
2205+}
2206+
2207+// AddAptSource adds an apt source. The public key for the
2208+// source is retrieved by fetching the given keyId from the
2209+// GPG key server at the given address.
2210+func (cfg *Config) AddAptSourceWithKeyId(name, keyId, keyServer string) {
2211+ src, _ := cfg.attrs["apt_sources"].([]*source)
2212+ cfg.attrs["apt_sources"] = append(src,
2213+ &source{
2214+ Source: name,
2215+ KeyId: keyId,
2216+ KeyServer: keyServer,
2217+ })
2218+}
2219+
2220+// SetDebconfSelections provides preseeded debconf answers
2221+// for the boot process. The given answers will be used as input
2222+// to debconf-set-selections(1).
2223+func (cfg *Config) SetDebconfSelections(answers string) {
2224+ cfg.set("debconf_selections", answers != "", answers)
2225+}
2226+
2227+// AddPackage adds a package to be installed on first boot.
2228+// If any packages are specified, "apt-get update"
2229+// will be called.
2230+func (cfg *Config) AddPackage(name string) {
2231+ pkgs, _ := cfg.attrs["packages"].([]string)
2232+ cfg.attrs["packages"] = append(pkgs, name)
2233+}
2234+
2235+func (cfg *Config) addCmd(kind string, c *command) {
2236+ cmds, _ := cfg.attrs[kind].([]*command)
2237+ cfg.attrs[kind] = append(cmds, c)
2238+}
2239+
2240+// AddRunCmd adds a command to be executed
2241+// at first boot. The command will be run
2242+// by the shell with any metacharacters retaining
2243+// their special meaning (that is, no quoting takes place).
2244+func (cfg *Config) AddRunCmd(cmd string) {
2245+ cfg.addCmd("runcmd", &command{literal: cmd})
2246+}
2247+
2248+// AddRunCmdArgs is like AddRunCmd except that the command
2249+// will be executed with the given arguments properly quoted.
2250+func (cfg *Config) AddRunCmdArgs(args ...string) {
2251+ cfg.addCmd("runcmd", &command{args: args})
2252+}
2253+
2254+// AddBootCmd is like AddRunCmd except that the
2255+// command will run very early in the boot process,
2256+// and it will run on every boot, not just the first time.
2257+func (cfg *Config) AddBootCmd(cmd string) {
2258+ cfg.addCmd("bootcmd", &command{literal: cmd})
2259+}
2260+
2261+// AddBootCmdArgs is like AddBootCmd except that the command
2262+// will be executed with the given arguments properly quoted.
2263+func (cfg *Config) AddBootCmdArgs(args ...string) {
2264+ cfg.addCmd("bootcmd", &command{args: args})
2265+}
2266+
2267+// SetDisableEC2Metadata sets whether access to the
2268+// EC2 metadata service is disabled early in boot
2269+// via a null route ( route del -host 169.254.169.254 reject).
2270+func (cfg *Config) SetDisableEC2Metadata(yes bool) {
2271+ cfg.set("disable_ec2_metadata", yes, yes)
2272+}
2273+
2274+// SetFinalMessage sets to message that will be written
2275+// when the system has finished booting for the first time.
2276+// By default, the message is:
2277+// "cloud-init boot finished at $TIMESTAMP. Up $UPTIME seconds".
2278+func (cfg *Config) SetFinalMessage(msg string) {
2279+ cfg.set("final_message", msg != "", msg)
2280+}
2281+
2282+// SetLocale sets the locale; it defaults to en_US.UTF-8.
2283+func (cfg *Config) SetLocale(locale string) {
2284+ cfg.set("locale", locale != "", locale)
2285+}
2286+
2287+// AddMount adds a mount point. The given
2288+// arguments will be used as a line in /etc/fstab.
2289+func (cfg *Config) AddMount(args ...string) {
2290+ mounts, _ := cfg.attrs["mounts"].([][]string)
2291+ cfg.attrs["mounts"] = append(mounts, args)
2292+}
2293+
2294+// OutputKind represents a destination for command output.
2295+type OutputKind string
2296+
2297+const (
2298+ OutInit OutputKind = "init"
2299+ OutConfig OutputKind = "config"
2300+ OutFinal OutputKind = "final"
2301+ OutAll OutputKind = "all"
2302+)
2303+
2304+// SetOutput specifies destination for command output.
2305+// Valid values for the kind "init", "config", "final" and "all".
2306+// Each of stdout and stderr can take one of the following forms:
2307+// >>file
2308+// appends to file
2309+// >file
2310+// overwrites file
2311+// |command
2312+// pipes to the given command.
2313+func (cfg *Config) SetOutput(kind OutputKind, stdout, stderr string) {
2314+ out, _ := cfg.attrs["output"].(map[string]interface{})
2315+ if out == nil {
2316+ out = make(map[string]interface{})
2317+ }
2318+ if stderr == "" {
2319+ out[string(kind)] = stdout
2320+ } else {
2321+ out[string(kind)] = []string{stdout, stderr}
2322+ }
2323+ cfg.attrs["output"] = out
2324+}
2325+
2326+// AddSSHKey adds a pre-generated ssh key to the
2327+// server keyring. Keys that are added like this will be
2328+// written to /etc/ssh and new random keys will not
2329+// be generated.
2330+func (cfg *Config) AddSSHKey(keyType SSHKeyType, keyData string) {
2331+ keys, _ := cfg.attrs["ssh_keys"].(map[SSHKeyType]string)
2332+ if keys == nil {
2333+ keys = make(map[SSHKeyType]string)
2334+ cfg.attrs["ssh_keys"] = keys
2335+ }
2336+ keys[keyType] = keyData
2337+}
2338+
2339+// SetDisableRoot sets whether ssh login is disabled to the root account
2340+// via the ssh authorized key associated with the instance metadata.
2341+// It is true by default.
2342+func (cfg *Config) SetDisableRoot(disable bool) {
2343+ // note that disable_root defaults to true, so we include
2344+ // the option only if disable is false.
2345+ cfg.set("disable_root", !disable, disable)
2346+}
2347+
2348+// AddSSHAuthorizedKey adds a key that will be
2349+// an entry in ~/.ssh/authorized_keys for the
2350+// configured user (see SetUser).
2351+func (cfg *Config) AddSSHAuthorizedKey(yes string) {
2352+ keys, _ := cfg.attrs["ssh_authorized_keys"].([]string)
2353+ cfg.attrs["ssh_authorized_keys"] = append(keys, yes)
2354+}
2355+
2356+// TODO
2357+// byobu
2358+// grub_dpkg
2359+// mcollective
2360+// phone_home
2361+// puppet
2362+// resizefs
2363+// rightscale_userdata
2364+// rsyslog
2365+// scripts_per_boot
2366+// scripts_per_instance
2367+// scripts_per_once
2368+// scripts_user
2369+// set_hostname
2370+// set_passwords
2371+// ssh_import_id
2372+// timezone
2373+// update_etc_hosts
2374+// update_hostname
2375
2376=== added directory 'control'
2377=== added file 'control/bootstrap.go'
2378--- control/bootstrap.go 1970-01-01 00:00:00 +0000
2379+++ control/bootstrap.go 2012-01-18 17:30:30 +0000
2380@@ -0,0 +1,28 @@
2381+package control
2382+
2383+import "flag"
2384+import "fmt"
2385+
2386+type BootstrapCommand struct {
2387+ environment string
2388+}
2389+
2390+var _ Command = (*BootstrapCommand)(nil)
2391+
2392+func (c *BootstrapCommand) Parse(args []string) error {
2393+ fs := flag.NewFlagSet("bootstrap", flag.ExitOnError)
2394+ fs.StringVar(&c.environment, "e", "", "juju environment to operate in")
2395+ fs.StringVar(&c.environment, "environment", "", "juju environment to operate in")
2396+ if err := fs.Parse(args); err != nil {
2397+ return err
2398+ }
2399+ if len(fs.Args()) != 0 {
2400+ return fmt.Errorf("Unknown args: %s", fs.Args())
2401+ }
2402+ return nil
2403+}
2404+
2405+func (c *BootstrapCommand) Run() error {
2406+ fmt.Println("Running bootstrap in environment ", c.environment)
2407+ return nil
2408+}
2409
2410=== added file 'control/command.go'
2411--- control/command.go 1970-01-01 00:00:00 +0000
2412+++ control/command.go 2012-01-18 17:30:30 +0000
2413@@ -0,0 +1,69 @@
2414+package control
2415+
2416+import "fmt"
2417+import "flag"
2418+
2419+type Command interface {
2420+ Parse(args []string) error
2421+ Run() error
2422+}
2423+
2424+type JujuCommand struct {
2425+ logfile string
2426+ verbose bool
2427+ subcmds map[string]Command
2428+ subcmd Command
2429+}
2430+
2431+func (c *JujuCommand) Logfile() string {
2432+ return c.logfile
2433+}
2434+
2435+func (c *JujuCommand) Verbose() bool {
2436+ return c.verbose
2437+}
2438+
2439+func (c *JujuCommand) Register(name string, subcmd Command) error {
2440+ if c.subcmds == nil {
2441+ c.subcmds = make(map[string]Command)
2442+ }
2443+ _, alreadythere := c.subcmds[name]
2444+ if alreadythere {
2445+ return fmt.Errorf("subcommand %s already registered", name)
2446+ }
2447+ c.subcmds[name] = subcmd
2448+ return nil
2449+}
2450+
2451+func (c *JujuCommand) Parse(args []string) error {
2452+ if len(args) == 0 {
2453+ return fmt.Errorf("no args to parse")
2454+ }
2455+ fs := flag.NewFlagSet(args[0], flag.ExitOnError)
2456+ fs.StringVar(&c.logfile, "l", "", "where to log to")
2457+ fs.StringVar(&c.logfile, "log-file", "", "where to log to")
2458+ fs.BoolVar(&c.verbose, "v", false, "whether to be noisy")
2459+ fs.BoolVar(&c.verbose, "verbose", false, "whether to be noisy")
2460+ if err := fs.Parse(args[1:]); err != nil {
2461+ return err
2462+ }
2463+ return c.parseSubcmd(fs.Args())
2464+}
2465+
2466+func (c *JujuCommand) parseSubcmd(args []string) error {
2467+ if len(args) == 0 {
2468+ return fmt.Errorf("no subcommand specified")
2469+ }
2470+ if c.subcmds == nil {
2471+ return fmt.Errorf("no subcommands registered")
2472+ }
2473+ exists := false
2474+ if c.subcmd, exists = c.subcmds[args[0]]; !exists {
2475+ return fmt.Errorf("no %s subcommand registered", args[0])
2476+ }
2477+ return c.subcmd.Parse(args[1:])
2478+}
2479+
2480+func (c *JujuCommand) Run() error {
2481+ return c.subcmd.Run()
2482+}
2483
2484=== added file 'control/command_test.go'
2485--- control/command_test.go 1970-01-01 00:00:00 +0000
2486+++ control/command_test.go 2012-01-18 17:30:30 +0000
2487@@ -0,0 +1,100 @@
2488+package control_test
2489+
2490+import (
2491+ "flag"
2492+ . "launchpad.net/gocheck"
2493+ "launchpad.net/juju/go/control"
2494+ "testing"
2495+)
2496+
2497+func Test(t *testing.T) { TestingT(t) }
2498+
2499+type CommandSuite struct{}
2500+
2501+var _ = Suite(&CommandSuite{})
2502+
2503+type testCommand struct {
2504+ value string
2505+}
2506+
2507+func (c *testCommand) Parse(args []string) error {
2508+ fs := flag.NewFlagSet("defenestrate", flag.ContinueOnError)
2509+ fs.StringVar(&c.value, "value", "", "doc")
2510+ return fs.Parse(args)
2511+}
2512+
2513+func (c *testCommand) Run() error {
2514+ return nil
2515+}
2516+
2517+func parseEmpty(args []string) (*control.JujuCommand, error) {
2518+ jc := new(control.JujuCommand)
2519+ err := jc.Parse(args)
2520+ return jc, err
2521+}
2522+
2523+func parseDefenestrate(args []string) (*control.JujuCommand, *testCommand, error) {
2524+ jc := new(control.JujuCommand)
2525+ tc := new(testCommand)
2526+ jc.Register("defenestrate", tc)
2527+ err := jc.Parse(args)
2528+ return jc, tc, err
2529+}
2530+
2531+func (s *CommandSuite) TestSubcommandDispatch(c *C) {
2532+ _, err := parseEmpty([]string{"juju"})
2533+ c.Assert(err, ErrorMatches, `no subcommand specified`)
2534+
2535+ _, err = parseEmpty([]string{"juju", "defenstrate"})
2536+ c.Assert(err, ErrorMatches, `no subcommands registered`)
2537+
2538+ _, _, err = parseDefenestrate([]string{"juju", "discombobulate"})
2539+ c.Assert(err, ErrorMatches, `no discombobulate subcommand registered`)
2540+
2541+ _, tc, err := parseDefenestrate([]string{"juju", "defenestrate"})
2542+ c.Assert(err, IsNil)
2543+ c.Assert(tc.value, Equals, "")
2544+
2545+ _, tc, err = parseDefenestrate([]string{"juju", "defenestrate", "--value", "firmly"})
2546+ c.Assert(err, IsNil)
2547+ c.Assert(tc.value, Equals, "firmly")
2548+
2549+ _, tc, err = parseDefenestrate([]string{"juju", "defenestrate", "--gibberish", "burble"})
2550+ c.Assert(err, ErrorMatches, "flag provided but not defined: -gibberish")
2551+}
2552+
2553+func (s *CommandSuite) TestVerbose(c *C) {
2554+ jc, err := parseEmpty([]string{"juju"})
2555+ c.Assert(err, ErrorMatches, "no subcommand specified")
2556+ c.Assert(jc.Verbose(), Equals, false)
2557+
2558+ jc, _, err = parseDefenestrate([]string{"juju", "defenestrate"})
2559+ c.Assert(err, IsNil)
2560+ c.Assert(jc.Verbose(), Equals, false)
2561+
2562+ jc, err = parseEmpty([]string{"juju", "--verbose"})
2563+ c.Assert(err, ErrorMatches, "no subcommand specified")
2564+ c.Assert(jc.Verbose(), Equals, true)
2565+
2566+ jc, _, err = parseDefenestrate([]string{"juju", "-v", "defenestrate"})
2567+ c.Assert(err, IsNil)
2568+ c.Assert(jc.Verbose(), Equals, true)
2569+}
2570+
2571+func (s *CommandSuite) TestLogfile(c *C) {
2572+ jc, err := parseEmpty([]string{"juju"})
2573+ c.Assert(err, ErrorMatches, "no subcommand specified")
2574+ c.Assert(jc.Logfile(), Equals, "")
2575+
2576+ jc, _, err = parseDefenestrate([]string{"juju", "defenestrate"})
2577+ c.Assert(err, IsNil)
2578+ c.Assert(jc.Logfile(), Equals, "")
2579+
2580+ jc, err = parseEmpty([]string{"juju", "-l", "foo"})
2581+ c.Assert(err, ErrorMatches, "no subcommand specified")
2582+ c.Assert(jc.Logfile(), Equals, "foo")
2583+
2584+ jc, _, err = parseDefenestrate([]string{"juju", "--log-file", "bar", "defenestrate"})
2585+ c.Assert(err, IsNil)
2586+ c.Assert(jc.Logfile(), Equals, "bar")
2587+}
2588
2589=== added directory 'environs'
2590=== added file 'environs/Makefile'
2591--- environs/Makefile 1970-01-01 00:00:00 +0000
2592+++ environs/Makefile 2012-01-18 17:30:30 +0000
2593@@ -0,0 +1,25 @@
2594+include $(GOROOT)/src/Make.inc
2595+
2596+all: package
2597+
2598+TARG=launchpad.net/juju/go/environs
2599+
2600+GOFILES=\
2601+ open.go\
2602+ config.go\
2603+ interface.go\
2604+
2605+GOFMT=gofmt
2606+BADFMT:=$(shell $(GOFMT) -l $(GOFILES) $(CGOFILES) $(wildcard *_test.go))
2607+
2608+gofmt: $(BADFMT)
2609+ @for F in $(BADFMT); do $(GOFMT) -w $$F && echo $$F; done
2610+
2611+ifneq ($(BADFMT),)
2612+ifneq ($(MAKECMDGOALS),gofmt)
2613+$(warning WARNING: make gofmt: $(BADFMT))
2614+endif
2615+endif
2616+
2617+include $(GOROOT)/src/Make.pkg
2618+
2619
2620=== added file 'environs/config.go'
2621--- environs/config.go 1970-01-01 00:00:00 +0000
2622+++ environs/config.go 2012-01-18 17:30:30 +0000
2623@@ -0,0 +1,134 @@
2624+package environs
2625+
2626+import (
2627+ "errors"
2628+ "fmt"
2629+ "io/ioutil"
2630+ "launchpad.net/goyaml"
2631+ "os"
2632+ "path/filepath"
2633+)
2634+
2635+// environ holds information about one environment.
2636+type environ struct {
2637+ kind string // the type of environment (e.g. ec2).
2638+ config interface{} // the configuration data for passing to NewEnviron.
2639+ err error // an error if the config data could not be parsed.
2640+}
2641+
2642+// Environs holds information about each named environment
2643+// in an environments.yaml file.
2644+type Environs struct {
2645+ Default string // The name of the default environment.
2646+ environs map[string]environ
2647+}
2648+
2649+// Names returns the list of environment names.
2650+func (e *Environs) Names() (names []string) {
2651+ for name := range e.environs {
2652+ names = append(names, name)
2653+ }
2654+ return
2655+}
2656+
2657+// providers maps from provider type to EnvironProvider for
2658+// each registered provider type.
2659+var providers = make(map[string]EnvironProvider)
2660+
2661+// RegisterProvider registers a new environment provider. Name gives the name
2662+// of the provider, and p the interface to that provider.
2663+//
2664+// RegisterProvider will panic if the same provider name is registered more than
2665+// once.
2666+func RegisterProvider(name string, p EnvironProvider) {
2667+ if providers[name] != nil {
2668+ panic(fmt.Errorf("juju: duplicate provider name %q", name))
2669+ }
2670+ providers[name] = p
2671+}
2672+
2673+// ReadEnvironsBytes parses the contents of an environments.yaml file
2674+// and returns its representation. An environment with an unknown type
2675+// will only generate an error when New is called for that environment.
2676+// Attributes for environments with known types are checked.
2677+func ReadEnvironsBytes(data []byte) (*Environs, error) {
2678+ var raw struct {
2679+ Default string "default"
2680+ Environments map[string]interface{} "environments"
2681+ }
2682+ raw.Environments = make(map[string]interface{}) // TODO fix bug in goyaml - it should make this automatically.
2683+ err := goyaml.Unmarshal(data, &raw)
2684+ if err != nil {
2685+ return nil, err
2686+ }
2687+
2688+ if raw.Default != "" && raw.Environments[raw.Default] == nil {
2689+ return nil, fmt.Errorf("default environment %q does not exist", raw.Default)
2690+ }
2691+ if raw.Default == "" {
2692+ // If there's a single environment, then we get the default
2693+ // automatically.
2694+ if len(raw.Environments) == 1 {
2695+ for name := range raw.Environments {
2696+ raw.Default = name
2697+ break
2698+ }
2699+ }
2700+ }
2701+
2702+ environs := make(map[string]environ)
2703+ for name, x := range raw.Environments {
2704+ attrs, ok := x.(map[interface{}]interface{})
2705+ if !ok {
2706+ return nil, fmt.Errorf("environment %q does not have attributes", name)
2707+ }
2708+ kind, _ := attrs["type"].(string)
2709+ if kind == "" {
2710+ return nil, fmt.Errorf("environment %q has no type", name)
2711+ }
2712+
2713+ p := providers[kind]
2714+ if p == nil {
2715+ // unknown provider type - skip entry but leave error message
2716+ // in case the environment is used later.
2717+ environs[name] = environ{
2718+ kind: kind,
2719+ err: fmt.Errorf("environment %q has an unknown provider type: %q", name, kind),
2720+ }
2721+ continue
2722+ }
2723+ cfg, err := p.ConfigChecker().Coerce(attrs, nil)
2724+ if err != nil {
2725+ return nil, fmt.Errorf("error parsing environment %q: %v", name, err)
2726+ }
2727+ environs[name] = environ{
2728+ kind: kind,
2729+ config: cfg,
2730+ }
2731+ }
2732+ return &Environs{raw.Default, environs}, nil
2733+}
2734+
2735+// ReadEnvirons reads the juju environments.yaml file
2736+// and returns the result of running ParseEnvironments
2737+// on the file's contents.
2738+// If environsFile is empty, $HOME/.juju/environments.yaml
2739+// is used.
2740+func ReadEnvirons(environsFile string) (*Environs, error) {
2741+ if environsFile == "" {
2742+ home := os.Getenv("HOME")
2743+ if home == "" {
2744+ return nil, errors.New("$HOME not set")
2745+ }
2746+ environsFile = filepath.Join(home, ".juju/environments.yaml")
2747+ }
2748+ data, err := ioutil.ReadFile(environsFile)
2749+ if err != nil {
2750+ return nil, err
2751+ }
2752+ e, err := ReadEnvironsBytes(data)
2753+ if err != nil {
2754+ return nil, fmt.Errorf("cannot parse %q: %v", environsFile, err)
2755+ }
2756+ return e, nil
2757+}
2758
2759=== added file 'environs/config_test.go'
2760--- environs/config_test.go 1970-01-01 00:00:00 +0000
2761+++ environs/config_test.go 2012-01-18 17:30:30 +0000
2762@@ -0,0 +1,179 @@
2763+package environs_test
2764+
2765+import (
2766+ "io/ioutil"
2767+ . "launchpad.net/gocheck"
2768+ "launchpad.net/juju/go/environs"
2769+ "os"
2770+ "path/filepath"
2771+)
2772+
2773+type configTest struct {
2774+ env string
2775+ check func(c *C, es *environs.Environs)
2776+}
2777+
2778+var configTests = []struct {
2779+ env string
2780+ check func(c *C, es *environs.Environs)
2781+}{
2782+ {`
2783+environments:
2784+ only:
2785+ type: unknown
2786+ other: anything
2787+`, func(c *C, es *environs.Environs) {
2788+ e, err := es.Open("")
2789+ c.Assert(e, IsNil)
2790+ c.Assert(err, NotNil)
2791+ c.Assert(err.Error(), Equals, `environment "only" has an unknown provider type: "unknown"`)
2792+ },
2793+ },
2794+ // one known environment, no defaults, bad attribute -> parse error
2795+ {`
2796+environments:
2797+ only:
2798+ type: dummy
2799+ badattr: anything
2800+`, nil,
2801+ },
2802+ // one known environment, no defaults -> parse ok, instantiate ok
2803+ {`
2804+environments:
2805+ only:
2806+ type: dummy
2807+ basename: foo
2808+`, func(c *C, es *environs.Environs) {
2809+ e, err := es.Open("")
2810+ c.Assert(err, IsNil)
2811+ checkDummyEnviron(c, e, "foo")
2812+ },
2813+ },
2814+ // several environments, no defaults -> parse ok, instantiate maybe error
2815+ {`
2816+environments:
2817+ one:
2818+ type: dummy
2819+ basename: foo
2820+ two:
2821+ type: dummy
2822+ basename: bar
2823+`, func(c *C, es *environs.Environs) {
2824+ e, err := es.Open("")
2825+ c.Assert(err, NotNil)
2826+ e, err = es.Open("one")
2827+ c.Assert(err, IsNil)
2828+ checkDummyEnviron(c, e, "foo")
2829+ },
2830+ },
2831+ // several environments, default -> parse ok, instantiate ok
2832+ {`
2833+default:
2834+ two
2835+environments:
2836+ one:
2837+ type: dummy
2838+ basename: foo
2839+ two:
2840+ type: dummy
2841+ basename: bar
2842+`, func(c *C, es *environs.Environs) {
2843+ e, err := es.Open("")
2844+ c.Assert(err, IsNil)
2845+ checkDummyEnviron(c, e, "bar")
2846+ },
2847+ },
2848+}
2849+
2850+func checkDummyEnviron(c *C, e environs.Environ, basename string) {
2851+ i0, err := e.StartInstance(0)
2852+ c.Assert(err, IsNil)
2853+ c.Assert(i0, NotNil)
2854+ c.Assert(i0.DNSName(), Equals, basename+"-0")
2855+
2856+ is, err := e.Instances()
2857+ c.Assert(err, IsNil)
2858+ c.Assert(len(is), Equals, 1)
2859+ c.Assert(is[0], Equals, i0)
2860+
2861+ i1, err := e.StartInstance(1)
2862+ c.Assert(err, IsNil)
2863+ c.Assert(i1, NotNil)
2864+ c.Assert(i1.DNSName(), Equals, basename+"-1")
2865+
2866+ is, err = e.Instances()
2867+ c.Assert(err, IsNil)
2868+ c.Assert(len(is), Equals, 2)
2869+ if is[0] == i1 {
2870+ is[0], is[1] = is[1], is[0]
2871+ }
2872+ c.Assert(is[0], Equals, i0)
2873+ c.Assert(is[1], Equals, i1)
2874+
2875+ err = e.StopInstances([]environs.Instance{i0})
2876+ c.Assert(err, IsNil)
2877+
2878+ is, err = e.Instances()
2879+ c.Assert(err, IsNil)
2880+ c.Assert(len(is), Equals, 1)
2881+ c.Assert(is[0], Equals, i1)
2882+
2883+ err = e.Destroy()
2884+ c.Assert(err, IsNil)
2885+}
2886+
2887+func (suite) TestConfig(c *C) {
2888+ for i, t := range configTests {
2889+ c.Logf("running test %v", i)
2890+ es, err := environs.ReadEnvironsBytes([]byte(t.env))
2891+ if es == nil {
2892+ c.Logf("parse failed\n")
2893+ if t.check != nil {
2894+ c.Errorf("test %d failed: %v", i, err)
2895+ }
2896+ } else {
2897+ if t.check == nil {
2898+ c.Errorf("test %d parsed ok but should have failed", i)
2899+ continue
2900+ }
2901+ c.Logf("checking...")
2902+ t.check(c, es)
2903+ }
2904+ }
2905+}
2906+
2907+func (suite) TestConfigFile(c *C) {
2908+ d := c.MkDir()
2909+ err := os.Mkdir(filepath.Join(d, ".juju"), 0777)
2910+ c.Assert(err, IsNil)
2911+
2912+ path := filepath.Join(d, ".juju", "environments.yaml")
2913+ env := `
2914+environments:
2915+ only:
2916+ type: dummy
2917+ basename: foo
2918+`
2919+ err = ioutil.WriteFile(path, []byte(env), 0666)
2920+ c.Assert(err, IsNil)
2921+
2922+ // test reading from a named file
2923+ es, err := environs.ReadEnvirons(path)
2924+ c.Assert(err, IsNil)
2925+ e, err := es.Open("")
2926+ c.Assert(err, IsNil)
2927+ checkDummyEnviron(c, e, "foo")
2928+
2929+ // test reading from the default environments.yaml file.
2930+ h := os.Getenv("HOME")
2931+ os.Setenv("HOME", d)
2932+
2933+ es, err = environs.ReadEnvirons("")
2934+ c.Assert(err, IsNil)
2935+ e, err = es.Open("")
2936+ c.Assert(err, IsNil)
2937+ checkDummyEnviron(c, e, "foo")
2938+
2939+ // reset $HOME just in case something else relies on it.
2940+ os.Setenv("HOME", h)
2941+}
2942
2943=== added file 'environs/dummyprovider_test.go'
2944--- environs/dummyprovider_test.go 1970-01-01 00:00:00 +0000
2945+++ environs/dummyprovider_test.go 2012-01-18 17:30:30 +0000
2946@@ -0,0 +1,93 @@
2947+// Dummy is a bare minimum provider that doesn't actually do anything.
2948+// The configuration requires a single value, "basename", which
2949+// is used as the base name of any machines that are "created".
2950+// It has no persistent state.
2951+//
2952+// Note that this file contains no tests as such - it is
2953+// just used by the testing code.
2954+package environs_test
2955+
2956+import (
2957+ "fmt"
2958+ "launchpad.net/juju/go/environs"
2959+ "launchpad.net/juju/go/schema"
2960+ "sync"
2961+)
2962+
2963+func init() {
2964+ environs.RegisterProvider("dummy", dummyProvider{})
2965+}
2966+
2967+type dummyInstance struct {
2968+ name string
2969+}
2970+
2971+func (m *dummyInstance) Id() string {
2972+ return fmt.Sprintf("dummy-%s", m.name)
2973+}
2974+
2975+func (m *dummyInstance) DNSName() string {
2976+ return m.name
2977+}
2978+
2979+type dummyProvider struct{}
2980+
2981+func (dummyProvider) ConfigChecker() schema.Checker {
2982+ return schema.FieldMap(
2983+ schema.Fields{
2984+ "type": schema.Const("dummy"),
2985+ "basename": schema.String(),
2986+ },
2987+ nil,
2988+ )
2989+}
2990+
2991+type dummyEnviron struct {
2992+ mu sync.Mutex
2993+ baseName string
2994+ n int // instance count
2995+
2996+ instances map[string]*dummyInstance
2997+}
2998+
2999+func (dummyProvider) Open(name string, attributes interface{}) (e environs.Environ, err error) {
3000+ cfg := attributes.(schema.MapType)
3001+ return &dummyEnviron{
3002+ baseName: cfg["basename"].(string),
3003+ instances: make(map[string]*dummyInstance),
3004+ }, nil
3005+}
3006+
3007+func (*dummyEnviron) Destroy() error {
3008+ return nil
3009+}
3010+
3011+func (e *dummyEnviron) StartInstance(id int) (environs.Instance, error) {
3012+ e.mu.Lock()
3013+ defer e.mu.Unlock()
3014+ i := &dummyInstance{
3015+ name: fmt.Sprintf("%s-%d", e.baseName, e.n),
3016+ }
3017+ e.instances[i.name] = i
3018+ e.n++
3019+ return i, nil
3020+}
3021+
3022+func (e *dummyEnviron) StopInstances(is []environs.Instance) error {
3023+ e.mu.Lock()
3024+ defer e.mu.Unlock()
3025+ for _, i := range is {
3026+ delete(e.instances, i.(*dummyInstance).name)
3027+ }
3028+ return nil
3029+}
3030+
3031+func (e *dummyEnviron) Instances() ([]environs.Instance, error) {
3032+ e.mu.Lock()
3033+ defer e.mu.Unlock()
3034+ var is []environs.Instance
3035+ for _, i := range e.instances {
3036+ is = append(is, i)
3037+ }
3038+ return is, nil
3039+}
3040
3041=== added directory 'environs/ec2'
3042=== added file 'environs/ec2/Makefile'
3043--- environs/ec2/Makefile 1970-01-01 00:00:00 +0000
3044+++ environs/ec2/Makefile 2012-01-18 17:30:30 +0000
3045@@ -0,0 +1,31 @@
3046+include $(GOROOT)/src/Make.inc
3047+
3048+all: package
3049+
3050+TARG=launchpad.net/juju/go/environs/ec2
3051+
3052+GOFILES=\
3053+ config.go\
3054+ ec2.go\
3055+ image.go\
3056+ util.go\
3057+
3058+GOFMT=gofmt
3059+BADFMT:=$(shell $(GOFMT) -l $(GOFILES) $(CGOFILES) $(wildcard *_test.go))
3060+
3061+gofmt: $(BADFMT)
3062+ @for F in $(BADFMT); do $(GOFMT) -w $$F && echo $$F; done
3063+
3064+ifneq ($(BADFMT),)
3065+ifneq ($(MAKECMDGOALS),gofmt)
3066+$(warning WARNING: make gofmt: $(BADFMT))
3067+endif
3068+endif
3069+
3070+include $(GOROOT)/src/Make.pkg
3071+
3072+# regenerate doesn't remove images that have disappeared
3073+# but that's probably not a problem.
3074+regenerate:
3075+ gotest -regenerate-images
3076+ bzr add images
3077
3078=== added file 'environs/ec2/config.go'
3079--- environs/ec2/config.go 1970-01-01 00:00:00 +0000
3080+++ environs/ec2/config.go 2012-01-18 17:30:30 +0000
3081@@ -0,0 +1,79 @@
3082+package ec2
3083+
3084+import (
3085+ "fmt"
3086+ "launchpad.net/goamz/aws"
3087+ "launchpad.net/juju/go/schema"
3088+)
3089+
3090+// providerConfig is a placeholder for any config information
3091+// that we will have in a configuration file.
3092+type providerConfig struct {
3093+ region string
3094+ auth aws.Auth
3095+}
3096+
3097+type checker struct{}
3098+
3099+func (checker) Coerce(v interface{}, path []string) (interface{}, error) {
3100+ return &providerConfig{}, nil
3101+}
3102+
3103+// TODO move these known strings into goamz/aws
3104+var Regions = map[string]aws.Region{
3105+ "ap-northeast-1": aws.APNortheast,
3106+ "ap-southeast-1": aws.APSoutheast,
3107+ "eu-west-1": aws.EUWest,
3108+ "us-east-1": aws.USEast,
3109+ "us-west-1": aws.USWest,
3110+}
3111+
3112+func (environProvider) ConfigChecker() schema.Checker {
3113+ return combineCheckers(
3114+ schema.FieldMap(
3115+ schema.Fields{
3116+ "access-key": schema.String(),
3117+ "secret-key": schema.String(),
3118+ "region": schema.String(),
3119+ }, []string{
3120+ "access-key",
3121+ "secret-key",
3122+ "region",
3123+ },
3124+ ),
3125+ checkerFunc(func(v interface{}, path []string) (newv interface{}, err error) {
3126+ m := v.(schema.MapType)
3127+ var c providerConfig
3128+
3129+ c.auth.AccessKey = maybeString(m["access-key"], "")
3130+ c.auth.SecretKey = maybeString(m["secret-key"], "")
3131+ if c.auth.AccessKey == "" || c.auth.SecretKey == "" {
3132+ if c.auth.AccessKey != "" {
3133+ return nil, fmt.Errorf("environment has access-key but no secret-key")
3134+ }
3135+ if c.auth.SecretKey != "" {
3136+ return nil, fmt.Errorf("environment has secret-key but no access-key")
3137+ }
3138+ var err error
3139+ c.auth, err = aws.EnvAuth()
3140+ if err != nil {
3141+ return nil, err
3142+ }
3143+ }
3144+
3145+ regionName := maybeString(m["region"], "us-east-1")
3146+ if _, ok := Regions[regionName]; !ok {
3147+ return nil, fmt.Errorf("invalid region name %q", regionName)
3148+ }
3149+ c.region = regionName
3150+ return &c, nil
3151+ }),
3152+ )
3153+}
3154+
3155+func maybeString(x interface{}, dflt string) string {
3156+ if x == nil {
3157+ return dflt
3158+ }
3159+ return x.(string)
3160+}
3161
3162=== added file 'environs/ec2/config_test.go'
3163--- environs/ec2/config_test.go 1970-01-01 00:00:00 +0000
3164+++ environs/ec2/config_test.go 2012-01-18 17:30:30 +0000
3165@@ -0,0 +1,104 @@
3166+package ec2
3167+
3168+import (
3169+ "launchpad.net/goamz/aws"
3170+ . "launchpad.net/gocheck"
3171+ "launchpad.net/juju/go/environs"
3172+ "os"
3173+ "strings"
3174+)
3175+
3176+// Use local suite since this file lives in the ec2 package
3177+// for testing internals.
3178+type configSuite struct{}
3179+
3180+var _ = Suite(configSuite{})
3181+
3182+var configTestRegion = aws.Region{
3183+ EC2Endpoint: "testregion.nowhere:1234",
3184+}
3185+
3186+var testAuth = aws.Auth{"gopher", "long teeth"}
3187+
3188+type configTest struct {
3189+ env string
3190+ config *providerConfig
3191+ err string
3192+}
3193+
3194+var configTests = []configTest{
3195+ {"", &providerConfig{region: "us-east-1", auth: testAuth}, ""},
3196+ {"region: eu-west-1\n", &providerConfig{region: "eu-west-1", auth: testAuth}, ""},
3197+ {"region: unknown\n", nil, ".*invalid region name.*"},
3198+ {"region: configtest\n", &providerConfig{region: "configtest", auth: testAuth}, ""},
3199+ {"region: 666\n", nil, ".*expected string, got 666"},
3200+ {"access-key: 666\n", nil, ".*expected string, got 666"},
3201+ {"secret-key: 666\n", nil, ".*expected string, got 666"},
3202+ {"access-key: jujuer\nsecret-key: open sesame\n",
3203+ &providerConfig{
3204+ region: "us-east-1",
3205+ auth: aws.Auth{
3206+ AccessKey: "jujuer",
3207+ SecretKey: "open sesame",
3208+ },
3209+ },
3210+ "",
3211+ },
3212+ {"access-key: jujuer\n", nil, ".*environment has access-key but no secret-key"},
3213+ {"secret-key: badness\n", nil, ".*environment has secret-key but no access-key"},
3214+ // unknown fields are discarded
3215+ {"unknown-something: 666\n", &providerConfig{region: "us-east-1", auth: testAuth}, ""},
3216+}
3217+
3218+func indent(s string, with string) string {
3219+ var r string
3220+ lines := strings.Split(s, "\n")
3221+ for _, l := range lines {
3222+ r += with + l + "\n"
3223+ }
3224+ return r
3225+}
3226+
3227+func makeEnv(s string) []byte {
3228+ return []byte("environments:\n testenv:\n type: ec2\n" + indent(s, " "))
3229+}
3230+
3231+func (configSuite) TestConfig(c *C) {
3232+ Regions["configtest"] = configTestRegion
3233+ defer delete(Regions, "configtest")
3234+
3235+ defer os.Setenv("AWS_ACCESS_KEY_ID", os.Getenv("AWS_ACCESS_KEY_ID"))
3236+ defer os.Setenv("AWS_SECRET_ACCESS_KEY", os.Getenv("AWS_SECRET_ACCESS_KEY"))
3237+
3238+ os.Setenv("AWS_ACCESS_KEY_ID", "")
3239+ os.Setenv("AWS_SECRET_ACCESS_KEY", "")
3240+
3241+ // first try with no auth environment vars set
3242+ test := configTest{"", &providerConfig{region: "us-east-1", auth: testAuth}, ".*not found in environment"}
3243+ test.run(c)
3244+
3245+ // then set testAuthults
3246+ os.Setenv("AWS_ACCESS_KEY_ID", testAuth.AccessKey)
3247+ os.Setenv("AWS_SECRET_ACCESS_KEY", testAuth.SecretKey)
3248+
3249+ for _, t := range configTests {
3250+ t.run(c)
3251+ }
3252+}
3253+
3254+func (t configTest) run(c *C) {
3255+ envs, err := environs.ReadEnvironsBytes(makeEnv(t.env))
3256+ if err != nil {
3257+ if t.err != "" {
3258+ c.Check(err, ErrorMatches, t.err, Bug("environ %q", t.env))
3259+ } else {
3260+ c.Check(err, IsNil, Bug("environ %q", t.env))
3261+ }
3262+ return
3263+ }
3264+ e, err := envs.Open("testenv")
3265+ c.Assert(err, IsNil)
3266+ c.Assert(e, NotNil)
3267+ c.Assert(e, FitsTypeOf, (*environ)(nil), Bug("environ %q", t.env))
3268+ c.Check(e.(*environ).config, Equals, t.config, Bug("environ %q", t.env))
3269+}
3270
3271=== added file 'environs/ec2/ec2.go'
3272--- environs/ec2/ec2.go 1970-01-01 00:00:00 +0000
3273+++ environs/ec2/ec2.go 2012-01-18 17:30:30 +0000
3274@@ -0,0 +1,171 @@
3275+package ec2
3276+
3277+import (
3278+ "fmt"
3279+ "launchpad.net/goamz/ec2"
3280+ "launchpad.net/juju/go/environs"
3281+)
3282+
3283+func init() {
3284+ environs.RegisterProvider("ec2", environProvider{})
3285+}
3286+
3287+type environProvider struct{}
3288+
3289+var _ environs.EnvironProvider = environProvider{}
3290+
3291+type environ struct {
3292+ name string
3293+ config *providerConfig
3294+ ec2 *ec2.EC2
3295+}
3296+
3297+var _ environs.Environ = (*environ)(nil)
3298+
3299+type instance struct {
3300+ *ec2.Instance
3301+}
3302+
3303+var _ environs.Instance = (*instance)(nil)
3304+
3305+func (inst *instance) Id() string {
3306+ return inst.InstanceId
3307+}
3308+
3309+func (inst *instance) DNSName() string {
3310+ return inst.Instance.DNSName
3311+}
3312+
3313+func (environProvider) Open(name string, config interface{}) (e environs.Environ, err error) {
3314+ cfg := config.(*providerConfig)
3315+ if Regions[cfg.region].EC2Endpoint == "" {
3316+ return nil, fmt.Errorf("no ec2 endpoint found for region %q, opening %q", cfg.region, name)
3317+ }
3318+ return &environ{
3319+ name: name,
3320+ config: cfg,
3321+ ec2: ec2.New(cfg.auth, Regions[cfg.region]),
3322+ }, nil
3323+}
3324+
3325+func (e *environ) StartInstance(machineId int) (environs.Instance, error) {
3326+ image, err := FindImageSpec(DefaultImageConstraint)
3327+ if err != nil {
3328+ return nil, fmt.Errorf("cannot find image: %v", err)
3329+ }
3330+ groups, err := e.setUpGroups(machineId)
3331+ if err != nil {
3332+ return nil, fmt.Errorf("cannot set up groups: %v", err)
3333+ }
3334+ instances, err := e.ec2.RunInstances(&ec2.RunInstances{
3335+ ImageId: image.ImageId,
3336+ MinCount: 1,
3337+ MaxCount: 1,
3338+ UserData: nil,
3339+ InstanceType: "m1.small",
3340+ SecurityGroups: groups,
3341+ })
3342+ if err != nil {
3343+ return nil, fmt.Errorf("cannot run instances: %v", err)
3344+ }
3345+ if len(instances.Instances) != 1 {
3346+ return nil, fmt.Errorf("expected 1 started instance, got %d", len(instances.Instances))
3347+ }
3348+ return &instance{&instances.Instances[0]}, nil
3349+}
3350+
3351+func (e *environ) StopInstances(insts []environs.Instance) error {
3352+ if len(insts) == 0 {
3353+ return nil
3354+ }
3355+ names := make([]string, len(insts))
3356+ for i, inst := range insts {
3357+ names[i] = inst.(*instance).InstanceId
3358+ }
3359+ _, err := e.ec2.TerminateInstances(names)
3360+ return err
3361+}
3362+
3363+func (e *environ) Instances() ([]environs.Instance, error) {
3364+ filter := ec2.NewFilter()
3365+ filter.Add("instance-state-name", "pending", "running")
3366+
3367+ resp, err := e.ec2.Instances(nil, filter)
3368+ if err != nil {
3369+ return nil, err
3370+ }
3371+ var insts []environs.Instance
3372+ for i := range resp.Reservations {
3373+ r := &resp.Reservations[i]
3374+ for j := range r.Instances {
3375+ insts = append(insts, &instance{&r.Instances[j]})
3376+ }
3377+ }
3378+ return insts, nil
3379+}
3380+
3381+func (e *environ) Destroy() error {
3382+ insts, err := e.Instances()
3383+ if err != nil {
3384+ return err
3385+ }
3386+ return e.StopInstances(insts)
3387+}
3388+
3389+func (e *environ) machineGroupName(machineId int) string {
3390+ return fmt.Sprintf("%s-%d", e.groupName(), machineId)
3391+}
3392+
3393+func (e *environ) groupName() string {
3394+ return "juju-" + e.name
3395+}
3396+
3397+// setUpGroups creates the security groups for the new machine, and
3398+// returns them.
3399+//
3400+// Instances are tagged with a group so they can be distinguished from
3401+// other instances that might be running on the same EC2 account. In
3402+// addition, a specific machine security group is created for each
3403+// machine, so that its firewall rules can be configured per machine.
3404+func (e *environ) setUpGroups(machineId int) ([]ec2.SecurityGroup, error) {
3405+ jujuGroup := ec2.SecurityGroup{Name: e.groupName()}
3406+ jujuMachineGroup := ec2.SecurityGroup{Name: e.machineGroupName(machineId)}
3407+ groups, err := e.ec2.SecurityGroups([]ec2.SecurityGroup{jujuGroup, jujuMachineGroup}, nil)
3408+ if err != nil {
3409+ return nil, fmt.Errorf("cannot get security groups: %v", err)
3410+ }
3411+
3412+ for _, g := range groups.Groups {
3413+ switch g.Name {
3414+ case jujuGroup.Name:
3415+ jujuGroup = g.SecurityGroup
3416+ case jujuMachineGroup.Name:
3417+ jujuMachineGroup = g.SecurityGroup
3418+ }
3419+ }
3420+
3421+ // Create the provider group if doesn't exist.
3422+ if jujuGroup.Id == "" {
3423+ r, err := e.ec2.CreateSecurityGroup(jujuGroup.Name, "juju group for "+e.name)
3424+ if err != nil {
3425+ return nil, fmt.Errorf("cannot create juju security group: %v", err)
3426+ }
3427+ jujuGroup = r.SecurityGroup
3428+ }
3429+
3430+ // Create the machine-specific group, but first see if there's
3431+ // one already existing from a previous machine launch;
3432+ // if so, delete it, since it can have the wrong firewall setup
3433+ if jujuMachineGroup.Id != "" {
3434+ _, err := e.ec2.DeleteSecurityGroup(jujuMachineGroup)
3435+ if err != nil {
3436+ return nil, fmt.Errorf("cannot delete old security group %q: %v", jujuMachineGroup.Name, err)
3437+ }
3438+ }
3439+ descr := fmt.Sprintf("juju group for %s machine %d", e.name, machineId)
3440+ r, err := e.ec2.CreateSecurityGroup(jujuMachineGroup.Name, descr)
3441+ if err != nil {
3442+ return nil, fmt.Errorf("cannot create machine group %q: %v", jujuMachineGroup.Name, err)
3443+ }
3444+ return []ec2.SecurityGroup{jujuGroup, r.SecurityGroup}, nil
3445+}
3446
3447=== added file 'environs/ec2/image.go'
3448--- environs/ec2/image.go 1970-01-01 00:00:00 +0000
3449+++ environs/ec2/image.go 2012-01-18 17:30:30 +0000
3450@@ -0,0 +1,80 @@
3451+package ec2
3452+
3453+import (
3454+ "bufio"
3455+ "fmt"
3456+ "net/http"
3457+ "strings"
3458+)
3459+
3460+// ImageConstraint specifies a range of possible machine images.
3461+// TODO allow specification of softer constraints?
3462+type ImageConstraint struct {
3463+ UbuntuRelease string
3464+ Architecture string
3465+ PersistentStorage bool
3466+ Region string
3467+ Daily bool
3468+ Desktop bool
3469+}
3470+
3471+var DefaultImageConstraint = &ImageConstraint{
3472+ UbuntuRelease: "oneiric",
3473+ Architecture: "i386",
3474+ PersistentStorage: true,
3475+ Region: "us-east-1",
3476+ Daily: false,
3477+ Desktop: false,
3478+}
3479+
3480+type ImageSpec struct {
3481+ ImageId string
3482+}
3483+
3484+func FindImageSpec(spec *ImageConstraint) (*ImageSpec, error) {
3485+ // note: original get_image_id added three optional args:
3486+ // DefaultImageId if found, returns that immediately
3487+ // Region overrides spec.Region
3488+ // DefaultSeries used if spec.UbuntuRelease is ""
3489+
3490+ hclient := new(http.Client)
3491+ uri := fmt.Sprintf("http://uec-images.ubuntu.com/query/%s/%s/%s.current.txt",
3492+ spec.UbuntuRelease,
3493+ either(spec.Desktop, "desktop", "server"), // variant.
3494+ either(spec.Daily, "daily", "released"), // version.
3495+ )
3496+ resp, err := hclient.Get(uri)
3497+ if err == nil && resp.StatusCode != 200 {
3498+ err = fmt.Errorf("%s", resp.Status)
3499+ }
3500+ if err != nil {
3501+ return nil, fmt.Errorf("error getting instance types: %v", err)
3502+ }
3503+ defer resp.Body.Close()
3504+ ebsMatch := either(spec.PersistentStorage, "ebs", "instance-store")
3505+ r := bufio.NewReader(resp.Body)
3506+ for {
3507+ line, _, err := r.ReadLine()
3508+ if err != nil {
3509+ return nil, fmt.Errorf("cannot find matching image: %v", err)
3510+ }
3511+ f := strings.Split(string(line), "\t")
3512+ if len(f) < 8 {
3513+ continue
3514+ }
3515+ if f[4] != ebsMatch {
3516+ continue
3517+ }
3518+ if f[5] == spec.Architecture && f[6] == spec.Region {
3519+ return &ImageSpec{f[7]}, nil
3520+ }
3521+ }
3522+ panic("not reached")
3523+}
3524+
3525+func either(yes bool, a, b string) string {
3526+ if yes {
3527+ return a
3528+ }
3529+ return b
3530+}
3531
3532=== added file 'environs/ec2/image_test.go'
3533--- environs/ec2/image_test.go 1970-01-01 00:00:00 +0000
3534+++ environs/ec2/image_test.go 2012-01-18 17:30:30 +0000
3535@@ -0,0 +1,138 @@
3536+package ec2_test
3537+
3538+import (
3539+ "fmt"
3540+ "io"
3541+ . "launchpad.net/gocheck"
3542+ "launchpad.net/juju/go/environs/ec2"
3543+ "net/http"
3544+ "os"
3545+ "path/filepath"
3546+ "testing"
3547+)
3548+
3549+// N.B. the image IDs in this test will need updating
3550+// if the image directory is regenerated.
3551+var imageTests = []struct {
3552+ constraint ec2.ImageConstraint
3553+ imageId string
3554+ err string
3555+}{
3556+ {*ec2.DefaultImageConstraint, "ami-a7f539ce", ""},
3557+ {ec2.ImageConstraint{
3558+ UbuntuRelease: "natty",
3559+ Architecture: "amd64",
3560+ PersistentStorage: false,
3561+ Region: "eu-west-1",
3562+ Daily: true,
3563+ Desktop: true,
3564+ }, "ami-19fdc16d", ""},
3565+ {ec2.ImageConstraint{
3566+ UbuntuRelease: "natty",
3567+ Architecture: "i386",
3568+ PersistentStorage: true,
3569+ Region: "ap-northeast-1",
3570+ Daily: true,
3571+ Desktop: true,
3572+ }, "ami-cc9621cd", ""},
3573+ {ec2.ImageConstraint{
3574+ UbuntuRelease: "natty",
3575+ Architecture: "i386",
3576+ PersistentStorage: false,
3577+ Region: "ap-northeast-1",
3578+ Daily: true,
3579+ Desktop: true,
3580+ }, "ami-62962163", ""},
3581+ {ec2.ImageConstraint{
3582+ UbuntuRelease: "natty",
3583+ Architecture: "amd64",
3584+ PersistentStorage: false,
3585+ Region: "ap-northeast-1",
3586+ Daily: true,
3587+ Desktop: true,
3588+ }, "ami-a69621a7", ""},
3589+ {ec2.ImageConstraint{
3590+ UbuntuRelease: "zingy",
3591+ Architecture: "amd64",
3592+ PersistentStorage: false,
3593+ Region: "eu-west-1",
3594+ Daily: true,
3595+ Desktop: true,
3596+ }, "", "error getting instance types:.*"},
3597+}
3598+
3599+func (suite) TestFindImageSpec(c *C) {
3600+ // set up http so that all requests will be satisfied from the images directory.
3601+ defer setTransport(setTransport(http.NewFileTransport(http.Dir("images"))))
3602+
3603+ for i, t := range imageTests {
3604+ id, err := ec2.FindImageSpec(&t.constraint)
3605+ if t.err != "" {
3606+ c.Check(err, ErrorMatches, t.err, Bug("test %d", i))
3607+ c.Check(id, IsNil, Bug("test %d", i))
3608+ continue
3609+ }
3610+ if !c.Check(err, IsNil, Bug("test %d", i)) {
3611+ continue
3612+ }
3613+ if !c.Check(id, NotNil, Bug("test %d", i)) {
3614+ continue
3615+ }
3616+ c.Check(id.ImageId, Equals, t.imageId)
3617+ }
3618+}
3619+
3620+func setTransport(t http.RoundTripper) (old http.RoundTripper) {
3621+ old = http.DefaultTransport
3622+ http.DefaultTransport = t
3623+ return
3624+}
3625+
3626+// regenerate all data inside the images directory.
3627+// N.B. this second-guesses the logic inside images.go
3628+func regenerateImages(t *testing.T) {
3629+ if err := os.RemoveAll(imagesRoot); err != nil {
3630+ t.Errorf("cannot remove old images: %v", err)
3631+ return
3632+ }
3633+ for _, variant := range []string{"desktop", "server"} {
3634+ for _, version := range []string{"daily", "released"} {
3635+ for _, release := range []string{"natty", "oneiric"} {
3636+ s := fmt.Sprintf("query/%s/%s/%s.current.txt", release, variant, version)
3637+ t.Logf("regenerating images from %q", s)
3638+ err := copylocal(s)
3639+ if err != nil {
3640+ t.Logf("regenerate: %v", err)
3641+ }
3642+ }
3643+ }
3644+ }
3645+}
3646+
3647+var imagesRoot = "images"
3648+
3649+func copylocal(s string) error {
3650+ r, err := http.Get("http://uec-images.ubuntu.com/" + s)
3651+ if err != nil {
3652+ return fmt.Errorf("get %q: %v", s, err)
3653+ }
3654+ defer r.Body.Close()
3655+ if r.StatusCode != 200 {
3656+ return fmt.Errorf("status on %q: %s", s, r.Status)
3657+ }
3658+ path := filepath.Join(filepath.FromSlash(imagesRoot), filepath.FromSlash(s))
3659+ d, _ := filepath.Split(path)
3660+ if err := os.MkdirAll(d, 0777); err != nil {
3661+ return err
3662+ }
3663+ file, err := os.Create(path)
3664+ if err != nil {
3665+ return err
3666+ }
3667+ defer file.Close()
3668+ _, err = io.Copy(file, r.Body)
3669+ if err != nil {
3670+ return fmt.Errorf("error copying image file: %v", err)
3671+ }
3672+ return nil
3673+}
3674
3675=== added directory 'environs/ec2/images'
3676=== added directory 'environs/ec2/images/query'
3677=== added directory 'environs/ec2/images/query/natty'
3678=== added directory 'environs/ec2/images/query/natty/desktop'
3679=== added file 'environs/ec2/images/query/natty/desktop/daily.current.txt'
3680--- environs/ec2/images/query/natty/desktop/daily.current.txt 1970-01-01 00:00:00 +0000
3681+++ environs/ec2/images/query/natty/desktop/daily.current.txt 2012-01-18 17:30:30 +0000
3682@@ -0,0 +1,25 @@
3683+natty desktop daily 20111205 ebs amd64 ap-northeast-1 ami-d89621d9 aki-d409a2d5 paravirtual
3684+natty desktop daily 20111205 ebs i386 ap-northeast-1 ami-cc9621cd aki-d209a2d3 paravirtual
3685+natty desktop daily 20111205 instance-store amd64 ap-northeast-1 ami-a69621a7 aki-d409a2d5 paravirtual
3686+natty desktop daily 20111205 instance-store i386 ap-northeast-1 ami-62962163 aki-d209a2d3 paravirtual
3687+natty desktop daily 20111205 ebs amd64 ap-southeast-1 ami-ac3471fe aki-11d5aa43 paravirtual
3688+natty desktop daily 20111205 ebs i386 ap-southeast-1 ami-a63471f4 aki-13d5aa41 paravirtual
3689+natty desktop daily 20111205 instance-store amd64 ap-southeast-1 ami-8a3471d8 aki-11d5aa43 paravirtual
3690+natty desktop daily 20111205 instance-store i386 ap-southeast-1 ami-ea3471b8 aki-13d5aa41 paravirtual
3691+natty desktop daily 20111205 ebs amd64 eu-west-1 ami-abfdc1df aki-4feec43b paravirtual
3692+natty desktop daily 20111205 ebs i386 eu-west-1 ami-c7fdc1b3 aki-4deec439 paravirtual
3693+natty desktop daily 20111205 instance-store amd64 eu-west-1 ami-19fdc16d aki-4feec43b paravirtual
3694+natty desktop daily 20111205 instance-store i386 eu-west-1 ami-79fdc10d aki-4deec439 paravirtual
3695+natty desktop daily 20111205 ebs amd64 us-east-1 ami-e7408b8e hvm
3696+natty desktop daily 20111205 ebs amd64 us-east-1 ami-37408b5e aki-427d952b paravirtual
3697+natty desktop daily 20111205 ebs i386 us-east-1 ami-5d408b34 aki-407d9529 paravirtual
3698+natty desktop daily 20111205 instance-store amd64 us-east-1 ami-e1418a88 aki-427d952b paravirtual
3699+natty desktop daily 20111205 instance-store i386 us-east-1 ami-914289f8 aki-407d9529 paravirtual
3700+natty desktop daily 20111205 ebs amd64 us-west-1 ami-23a9f666 aki-9ba0f1de paravirtual
3701+natty desktop daily 20111205 ebs i386 us-west-1 ami-1ba9f65e aki-99a0f1dc paravirtual
3702+natty desktop daily 20111205 instance-store amd64 us-west-1 ami-03a9f646 aki-9ba0f1de paravirtual
3703+natty desktop daily 20111205 instance-store i386 us-west-1 ami-77a9f632 aki-99a0f1dc paravirtual
3704+natty desktop daily 20111205 ebs amd64 us-west-2 ami-aa98159a aki-ace26f9c paravirtual
3705+natty desktop daily 20111205 ebs i386 us-west-2 ami-a2981592 aki-dce26fec paravirtual
3706+natty desktop daily 20111205 instance-store amd64 us-west-2 ami-bc98158c aki-ace26f9c paravirtual
3707+natty desktop daily 20111205 instance-store i386 us-west-2 ami-b0981580 aki-dce26fec paravirtual
3708
3709=== added directory 'environs/ec2/images/query/natty/server'
3710=== added file 'environs/ec2/images/query/natty/server/daily.current.txt'
3711--- environs/ec2/images/query/natty/server/daily.current.txt 1970-01-01 00:00:00 +0000
3712+++ environs/ec2/images/query/natty/server/daily.current.txt 2012-01-18 17:30:30 +0000
3713@@ -0,0 +1,25 @@
3714+natty server daily 20111201 ebs amd64 ap-northeast-1 ami-a6b80fa7 aki-d409a2d5 paravirtual
3715+natty server daily 20111201 ebs i386 ap-northeast-1 ami-9cb80f9d aki-d209a2d3 paravirtual
3716+natty server daily 20111201 instance-store amd64 ap-northeast-1 ami-7cb80f7d aki-d409a2d5 paravirtual
3717+natty server daily 20111201 instance-store i386 ap-northeast-1 ami-68b80f69 aki-d209a2d3 paravirtual
3718+natty server daily 20111201 ebs amd64 ap-southeast-1 ami-62246130 aki-11d5aa43 paravirtual
3719+natty server daily 20111201 ebs i386 ap-southeast-1 ami-7e24612c aki-13d5aa41 paravirtual
3720+natty server daily 20111201 instance-store amd64 ap-southeast-1 ami-74246126 aki-11d5aa43 paravirtual
3721+natty server daily 20111201 instance-store i386 ap-southeast-1 ami-5e24610c aki-13d5aa41 paravirtual
3722+natty server daily 20111201 ebs amd64 eu-west-1 ami-431d2137 aki-4feec43b paravirtual
3723+natty server daily 20111201 ebs i386 eu-west-1 ami-691d211d aki-4deec439 paravirtual
3724+natty server daily 20111201 instance-store amd64 eu-west-1 ami-891c20fd aki-4feec43b paravirtual
3725+natty server daily 20111201 instance-store i386 eu-west-1 ami-9b1c20ef aki-4deec439 paravirtual
3726+natty server daily 20111201 ebs amd64 us-east-1 ami-0fec2766 hvm
3727+natty server daily 20111201 ebs amd64 us-east-1 ami-59ec2730 aki-427d952b paravirtual
3728+natty server daily 20111201 ebs i386 us-east-1 ami-95ed26fc aki-407d9529 paravirtual
3729+natty server daily 20111201 instance-store amd64 us-east-1 ami-1fed2676 aki-427d952b paravirtual
3730+natty server daily 20111201 instance-store i386 us-east-1 ami-3fed2656 aki-407d9529 paravirtual
3731+natty server daily 20111201 ebs amd64 us-west-1 ami-c983dc8c aki-9ba0f1de paravirtual
3732+natty server daily 20111201 ebs i386 us-west-1 ami-3583dc70 aki-99a0f1dc paravirtual
3733+natty server daily 20111201 instance-store amd64 us-west-1 ami-2f83dc6a aki-9ba0f1de paravirtual
3734+natty server daily 20111201 instance-store i386 us-west-1 ami-2183dc64 aki-99a0f1dc paravirtual
3735+natty server daily 20111201 ebs amd64 us-west-2 ami-689d1058 aki-ace26f9c paravirtual
3736+natty server daily 20111201 ebs i386 us-west-2 ami-649d1054 aki-dce26fec paravirtual
3737+natty server daily 20111201 instance-store amd64 us-west-2 ami-7e9d104e aki-ace26f9c paravirtual
3738+natty server daily 20111201 instance-store i386 us-west-2 ami-789d1048 aki-dce26fec paravirtual
3739
3740=== added file 'environs/ec2/images/query/natty/server/released.current.txt'
3741--- environs/ec2/images/query/natty/server/released.current.txt 1970-01-01 00:00:00 +0000
3742+++ environs/ec2/images/query/natty/server/released.current.txt 2012-01-18 17:30:30 +0000
3743@@ -0,0 +1,25 @@
3744+natty server release 20111003 ebs amd64 ap-northeast-1 ami-02b10503 aki-d409a2d5 paravirtual
3745+natty server release 20111003 ebs i386 ap-northeast-1 ami-00b10501 aki-d209a2d3 paravirtual
3746+natty server release 20111003 instance-store amd64 ap-northeast-1 ami-fab004fb aki-d409a2d5 paravirtual
3747+natty server release 20111003 instance-store i386 ap-northeast-1 ami-f0b004f1 aki-d209a2d3 paravirtual
3748+natty server release 20111003 ebs amd64 ap-southeast-1 ami-04255f56 aki-11d5aa43 paravirtual
3749+natty server release 20111003 ebs i386 ap-southeast-1 ami-06255f54 aki-13d5aa41 paravirtual
3750+natty server release 20111003 instance-store amd64 ap-southeast-1 ami-7a255f28 aki-11d5aa43 paravirtual
3751+natty server release 20111003 instance-store i386 ap-southeast-1 ami-72255f20 aki-13d5aa41 paravirtual
3752+natty server release 20111003 ebs amd64 eu-west-1 ami-a6f7c5d2 aki-4feec43b paravirtual
3753+natty server release 20111003 ebs i386 eu-west-1 ami-a4f7c5d0 aki-4deec439 paravirtual
3754+natty server release 20111003 instance-store amd64 eu-west-1 ami-c0f7c5b4 aki-4feec43b paravirtual
3755+natty server release 20111003 instance-store i386 eu-west-1 ami-fef7c58a aki-4deec439 paravirtual
3756+natty server release 20111003 ebs amd64 us-east-1 ami-f1589598 hvm
3757+natty server release 20111003 ebs amd64 us-east-1 ami-fd589594 aki-427d952b paravirtual
3758+natty server release 20111003 ebs i386 us-east-1 ami-e358958a aki-407d9529 paravirtual
3759+natty server release 20111003 instance-store amd64 us-east-1 ami-71589518 aki-427d952b paravirtual
3760+natty server release 20111003 instance-store i386 us-east-1 ami-c15994a8 aki-407d9529 paravirtual
3761+natty server release 20111003 ebs amd64 us-west-1 ami-4d580408 aki-9ba0f1de paravirtual
3762+natty server release 20111003 ebs i386 us-west-1 ami-43580406 aki-99a0f1dc paravirtual
3763+natty server release 20111003 instance-store amd64 us-west-1 ami-a15f03e4 aki-9ba0f1de paravirtual
3764+natty server release 20111003 instance-store i386 us-west-1 ami-e95f03ac aki-99a0f1dc paravirtual
3765+natty server release 20111003 ebs amd64 us-west-2 ami-1af9742a aki-ace26f9c paravirtual
3766+natty server release 20111003 ebs i386 us-west-2 ami-18f97428 aki-dce26fec paravirtual
3767+natty server release 20111003 instance-store amd64 us-west-2 ami-16f67b26 aki-ace26f9c paravirtual
3768+natty server release 20111003 instance-store i386 us-west-2 ami-10f67b20 aki-dce26fec paravirtual
3769
3770=== added directory 'environs/ec2/images/query/oneiric'
3771=== added directory 'environs/ec2/images/query/oneiric/desktop'
3772=== added file 'environs/ec2/images/query/oneiric/desktop/daily.current.txt'
3773--- environs/ec2/images/query/oneiric/desktop/daily.current.txt 1970-01-01 00:00:00 +0000
3774+++ environs/ec2/images/query/oneiric/desktop/daily.current.txt 2012-01-18 17:30:30 +0000
3775@@ -0,0 +1,25 @@
3776+oneiric desktop daily 20111120 ebs amd64 ap-northeast-1 ami-eaec5beb aki-ee5df7ef paravirtual
3777+oneiric desktop daily 20111120 ebs i386 ap-northeast-1 ami-d4ec5bd5 aki-ec5df7ed paravirtual
3778+oneiric desktop daily 20111120 instance-store amd64 ap-northeast-1 ami-c0ec5bc1 aki-ee5df7ef paravirtual
3779+oneiric desktop daily 20111120 instance-store i386 ap-northeast-1 ami-9eec5b9f aki-ec5df7ed paravirtual
3780+oneiric desktop daily 20111120 ebs amd64 ap-southeast-1 ami-48c9b31a aki-aa225af8 paravirtual
3781+oneiric desktop daily 20111120 ebs i386 ap-southeast-1 ami-58c9b30a aki-a4225af6 paravirtual
3782+oneiric desktop daily 20111120 instance-store amd64 ap-southeast-1 ami-b8c8b2ea aki-aa225af8 paravirtual
3783+oneiric desktop daily 20111120 instance-store i386 ap-southeast-1 ami-9ec8b2cc aki-a4225af6 paravirtual
3784+oneiric desktop daily 20111120 ebs amd64 eu-west-1 ami-817b47f5 aki-62695816 paravirtual
3785+oneiric desktop daily 20111120 ebs i386 eu-west-1 ami-af7b47db aki-64695810 paravirtual
3786+oneiric desktop daily 20111120 instance-store amd64 eu-west-1 ami-137b4767 aki-62695816 paravirtual
3787+oneiric desktop daily 20111120 instance-store i386 eu-west-1 ami-6f7b471b aki-64695810 paravirtual
3788+oneiric desktop daily 20111120 ebs amd64 us-east-1 ami-abed25c2 hvm
3789+oneiric desktop daily 20111120 ebs amd64 us-east-1 ami-dded25b4 aki-825ea7eb paravirtual
3790+oneiric desktop daily 20111120 ebs i386 us-east-1 ami-f9ed2590 aki-805ea7e9 paravirtual
3791+oneiric desktop daily 20111120 instance-store amd64 us-east-1 ami-6dec2404 aki-825ea7eb paravirtual
3792+oneiric desktop daily 20111120 instance-store i386 us-east-1 ami-53eb233a aki-805ea7e9 paravirtual
3793+oneiric desktop daily 20111120 ebs amd64 us-west-1 ami-7b421d3e aki-8d396bc8 paravirtual
3794+oneiric desktop daily 20111120 ebs i386 us-west-1 ami-79421d3c aki-83396bc6 paravirtual
3795+oneiric desktop daily 20111120 instance-store amd64 us-west-1 ami-8b431cce aki-8d396bc8 paravirtual
3796+oneiric desktop daily 20111120 instance-store i386 us-west-1 ami-e7431ca2 aki-83396bc6 paravirtual
3797+oneiric desktop daily 20111120 ebs amd64 us-west-2 ami-be89048e aki-98e26fa8 paravirtual
3798+oneiric desktop daily 20111120 ebs i386 us-west-2 ami-bc89048c aki-c2e26ff2 paravirtual
3799+oneiric desktop daily 20111120 instance-store amd64 us-west-2 ami-48890478 aki-98e26fa8 paravirtual
3800+oneiric desktop daily 20111120 instance-store i386 us-west-2 ami-58890468 aki-c2e26ff2 paravirtual
3801
3802=== added directory 'environs/ec2/images/query/oneiric/server'
3803=== added file 'environs/ec2/images/query/oneiric/server/daily.current.txt'
3804--- environs/ec2/images/query/oneiric/server/daily.current.txt 1970-01-01 00:00:00 +0000
3805+++ environs/ec2/images/query/oneiric/server/daily.current.txt 2012-01-18 17:30:30 +0000
3806@@ -0,0 +1,25 @@
3807+oneiric server daily 20111205 ebs amd64 ap-northeast-1 ami-42942343 aki-ee5df7ef paravirtual
3808+oneiric server daily 20111205 ebs i386 ap-northeast-1 ami-3a94233b aki-ec5df7ed paravirtual
3809+oneiric server daily 20111205 instance-store amd64 ap-northeast-1 ami-24942325 aki-ee5df7ef paravirtual
3810+oneiric server daily 20111205 instance-store i386 ap-northeast-1 ami-20942321 aki-ec5df7ed paravirtual
3811+oneiric server daily 20111205 ebs amd64 ap-southeast-1 ami-9c3673ce aki-aa225af8 paravirtual
3812+oneiric server daily 20111205 ebs i386 ap-southeast-1 ami-9e3673cc aki-a4225af6 paravirtual
3813+oneiric server daily 20111205 instance-store amd64 ap-southeast-1 ami-fc3673ae aki-aa225af8 paravirtual
3814+oneiric server daily 20111205 instance-store i386 ap-southeast-1 ami-f83673aa aki-a4225af6 paravirtual
3815+oneiric server daily 20111205 ebs amd64 eu-west-1 ami-87f9c5f3 aki-62695816 paravirtual
3816+oneiric server daily 20111205 ebs i386 eu-west-1 ami-aff9c5db aki-64695810 paravirtual
3817+oneiric server daily 20111205 instance-store amd64 eu-west-1 ami-b5f9c5c1 aki-62695816 paravirtual
3818+oneiric server daily 20111205 instance-store i386 eu-west-1 ami-d5f9c5a1 aki-64695810 paravirtual
3819+oneiric server daily 20111205 ebs amd64 us-east-1 ami-015d9668 hvm
3820+oneiric server daily 20111205 ebs amd64 us-east-1 ami-2f5d9646 aki-825ea7eb paravirtual
3821+oneiric server daily 20111205 ebs i386 us-east-1 ami-695d9600 aki-805ea7e9 paravirtual
3822+oneiric server daily 20111205 instance-store amd64 us-east-1 ami-c95e95a0 aki-825ea7eb paravirtual
3823+oneiric server daily 20111205 instance-store i386 us-east-1 ami-4b5e9522 aki-805ea7e9 paravirtual
3824+oneiric server daily 20111205 ebs amd64 us-west-1 ami-bba7f8fe aki-8d396bc8 paravirtual
3825+oneiric server daily 20111205 ebs i386 us-west-1 ami-b3a7f8f6 aki-83396bc6 paravirtual
3826+oneiric server daily 20111205 instance-store amd64 us-west-1 ami-9da7f8d8 aki-8d396bc8 paravirtual
3827+oneiric server daily 20111205 instance-store i386 us-west-1 ami-85a7f8c0 aki-83396bc6 paravirtual
3828+oneiric server daily 20111205 ebs amd64 us-west-2 ami-169b1626 aki-98e26fa8 paravirtual
3829+oneiric server daily 20111205 ebs i386 us-west-2 ami-109b1620 aki-c2e26ff2 paravirtual
3830+oneiric server daily 20111205 instance-store amd64 us-west-2 ami-209b1610 aki-98e26fa8 paravirtual
3831+oneiric server daily 20111205 instance-store i386 us-west-2 ami-3c9b160c aki-c2e26ff2 paravirtual
3832
3833=== added file 'environs/ec2/images/query/oneiric/server/released.current.txt'
3834--- environs/ec2/images/query/oneiric/server/released.current.txt 1970-01-01 00:00:00 +0000
3835+++ environs/ec2/images/query/oneiric/server/released.current.txt 2012-01-18 17:30:30 +0000
3836@@ -0,0 +1,25 @@
3837+oneiric server release 20111011 ebs amd64 ap-northeast-1 ami-30902431 aki-ee5df7ef paravirtual
3838+oneiric server release 20111011 ebs i386 ap-northeast-1 ami-2e90242f aki-ec5df7ed paravirtual
3839+oneiric server release 20111011 instance-store amd64 ap-northeast-1 ami-fa9723fb aki-ee5df7ef paravirtual
3840+oneiric server release 20111011 instance-store i386 ap-northeast-1 ami-e49723e5 aki-ec5df7ed paravirtual
3841+oneiric server release 20111011 ebs amd64 ap-southeast-1 ami-7a057f28 aki-aa225af8 paravirtual
3842+oneiric server release 20111011 ebs i386 ap-southeast-1 ami-76057f24 aki-a4225af6 paravirtual
3843+oneiric server release 20111011 instance-store amd64 ap-southeast-1 ami-54057f06 aki-aa225af8 paravirtual
3844+oneiric server release 20111011 instance-store i386 ap-southeast-1 ami-82047ed0 aki-a4225af6 paravirtual
3845+oneiric server release 20111011 ebs amd64 eu-west-1 ami-61b28015 aki-62695816 paravirtual
3846+oneiric server release 20111011 ebs i386 eu-west-1 ami-65b28011 aki-64695810 paravirtual
3847+oneiric server release 20111011 instance-store amd64 eu-west-1 ami-75b28001 aki-62695816 paravirtual
3848+oneiric server release 20111011 instance-store i386 eu-west-1 ami-dfcdffab aki-64695810 paravirtual
3849+oneiric server release 20111011 ebs amd64 us-east-1 ami-bff539d6 hvm
3850+oneiric server release 20111011 ebs amd64 us-east-1 ami-bbf539d2 aki-825ea7eb paravirtual
3851+oneiric server release 20111011 ebs i386 us-east-1 ami-a7f539ce aki-805ea7e9 paravirtual
3852+oneiric server release 20111011 instance-store amd64 us-east-1 ami-21f53948 aki-825ea7eb paravirtual
3853+oneiric server release 20111011 instance-store i386 us-east-1 ami-29f43840 aki-805ea7e9 paravirtual
3854+oneiric server release 20111011 ebs amd64 us-west-1 ami-7b772b3e aki-8d396bc8 paravirtual
3855+oneiric server release 20111011 ebs i386 us-west-1 ami-79772b3c aki-83396bc6 paravirtual
3856+oneiric server release 20111011 instance-store amd64 us-west-1 ami-4b772b0e aki-8d396bc8 paravirtual
3857+oneiric server release 20111011 instance-store i386 us-west-1 ami-a7762ae2 aki-83396bc6 paravirtual
3858+oneiric server release 20111011 ebs amd64 us-west-2 ami-2af9741a aki-98e26fa8 paravirtual
3859+oneiric server release 20111011 ebs i386 us-west-2 ami-20f97410 aki-c2e26ff2 paravirtual
3860+oneiric server release 20111011 instance-store amd64 us-west-2 ami-56f67b66 aki-98e26fa8 paravirtual
3861+oneiric server release 20111011 instance-store i386 us-west-2 ami-52f67b62 aki-c2e26ff2 paravirtual
3862
3863=== added file 'environs/ec2/live_test.go'
3864--- environs/ec2/live_test.go 1970-01-01 00:00:00 +0000
3865+++ environs/ec2/live_test.go 2012-01-18 17:30:30 +0000
3866@@ -0,0 +1,34 @@
3867+package ec2_test
3868+
3869+import (
3870+ "fmt"
3871+ . "launchpad.net/gocheck"
3872+ "launchpad.net/juju/go/environs"
3873+ "launchpad.net/juju/go/environs/jujutest"
3874+)
3875+
3876+// integrationConfig holds the environments configuration
3877+// for running the amazon EC2 integration tests.
3878+//
3879+// This is missing keys for security reasons; set the following environment variables
3880+// to make the integration testing work:
3881+// access-key: $AWS_ACCESS_KEY_ID
3882+// admin-secret: $AWS_SECRET_ACCESS_KEY
3883+var integrationConfig = []byte(`
3884+environments:
3885+ sample:
3886+ type: ec2
3887+`)
3888+
3889+func registerIntegrationTests() {
3890+ envs, err := environs.ReadEnvironsBytes(integrationConfig)
3891+ if err != nil {
3892+ panic(fmt.Errorf("cannot parse integration tests config data: %v", err))
3893+ }
3894+ for _, name := range envs.Names() {
3895+ Suite(&jujutest.LiveTests{
3896+ Environs: envs,
3897+ Name: name,
3898+ })
3899+ }
3900+}
3901
3902=== added file 'environs/ec2/local_test.go'
3903--- environs/ec2/local_test.go 1970-01-01 00:00:00 +0000
3904+++ environs/ec2/local_test.go 2012-01-18 17:30:30 +0000
3905@@ -0,0 +1,242 @@
3906+package ec2_test
3907+
3908+import (
3909+ "fmt"
3910+ "launchpad.net/goamz/aws"
3911+ amzec2 "launchpad.net/goamz/ec2"
3912+ "launchpad.net/goamz/ec2/ec2test"
3913+ . "launchpad.net/gocheck"
3914+ "launchpad.net/juju/go/environs"
3915+ "launchpad.net/juju/go/environs/ec2"
3916+ "launchpad.net/juju/go/environs/jujutest"
3917+)
3918+
3919+var functionalConfig = []byte(`
3920+environments:
3921+ sample:
3922+ type: ec2
3923+ region: test
3924+`)
3925+
3926+// localTests wraps jujutest.Tests by adding
3927+// set up and tear down functions that start a new
3928+// ec2test server for each test.
3929+// The server is accessed by using the "test" region,
3930+// which is changed to point to the network address
3931+// of the local server.
3932+type localTests struct {
3933+ *jujutest.Tests
3934+ srv localServer
3935+}
3936+
3937+// localLiveTests performs the live test suite, but locally.
3938+type localLiveTests struct {
3939+ *jujutest.LiveTests
3940+ srv localServer
3941+}
3942+
3943+type localServer struct {
3944+ srv *ec2test.Server
3945+ setup func(*ec2test.Server)
3946+}
3947+
3948+// Each test is run in each of the following scenarios.
3949+// A scenario is implemented by mutating the ec2test
3950+// server after it starts.
3951+var scenarios = []struct {
3952+ name string
3953+ setup func(*ec2test.Server)
3954+}{
3955+ {"normal", normalScenario},
3956+ {"initial-state-running", initialStateRunningScenario},
3957+ {"extra-instances", extraInstancesScenario},
3958+}
3959+
3960+func normalScenario(*ec2test.Server) {
3961+}
3962+
3963+func initialStateRunningScenario(srv *ec2test.Server) {
3964+ srv.SetInitialInstanceState(ec2test.Running)
3965+}
3966+
3967+func extraInstancesScenario(srv *ec2test.Server) {
3968+ states := []amzec2.InstanceState{
3969+ ec2test.ShuttingDown,
3970+ ec2test.Terminated,
3971+ ec2test.Stopped,
3972+ }
3973+ for _, state := range states {
3974+ srv.NewInstances(1, "m1.small", "ami-a7f539ce", state, nil)
3975+ }
3976+}
3977+
3978+func registerLocalTests() {
3979+ ec2.Regions["test"] = aws.Region{}
3980+ envs, err := environs.ReadEnvironsBytes(functionalConfig)
3981+ if err != nil {
3982+ panic(fmt.Errorf("cannot parse functional tests config data: %v", err))
3983+ }
3984+
3985+ for _, name := range envs.Names() {
3986+ for _, scen := range scenarios {
3987+ Suite(&localTests{
3988+ srv: localServer{setup: scen.setup},
3989+ Tests: &jujutest.Tests{
3990+ Environs: envs,
3991+ Name: name,
3992+ },
3993+ })
3994+ Suite(&localLiveTests{
3995+ srv: localServer{setup: scen.setup},
3996+ LiveTests: &jujutest.LiveTests{
3997+ Environs: envs,
3998+ Name: name,
3999+ },
4000+ })
4001+ }
4002+ }
4003+}
4004+
4005+func (t *localTests) TestInstanceGroups(c *C) {
4006+ env, err := t.Environs.Open(t.Name)
4007+ c.Assert(err, IsNil)
4008+
4009+ ec2conn := amzec2.New(aws.Auth{}, ec2.Regions["test"])
4010+
4011+ groups := amzec2.SecurityGroupNames(
4012+ fmt.Sprintf("juju-%s", t.Name),
4013+ fmt.Sprintf("juju-%s-%d", t.Name, 98),
4014+ fmt.Sprintf("juju-%s-%d", t.Name, 99),
4015+ )
4016+
4017+ inst0, err := env.StartInstance(98)
4018+ c.Assert(err, IsNil)
4019+ defer env.StopInstances([]environs.Instance{inst0})
4020+
4021+ // create a same-named group for the second instance
4022+ // before starting it, to check that it's deleted and
4023+ // recreated correctly.
4024+ oldGroup := ensureGroupExists(c, ec2conn, groups[2], "old group")
4025+
4026+ inst1, err := env.StartInstance(99)
4027+ c.Assert(err, IsNil)
4028+ defer env.StopInstances([]environs.Instance{inst1})
4029+
4030+ // go behind the scenes to check the machines have
4031+ // been put into the correct groups.
4032+
4033+ // first check that the old group has been deleted
4034+ groupsResp, err := ec2conn.SecurityGroups([]amzec2.SecurityGroup{oldGroup}, nil)
4035+ c.Assert(err, IsNil)
4036+ c.Check(len(groupsResp.Groups), Equals, 0)
4037+
4038+ // then check that the groups have been created.
4039+ groupsResp, err = ec2conn.SecurityGroups(groups, nil)
4040+ c.Assert(err, IsNil)
4041+ c.Assert(len(groupsResp.Groups), Equals, len(groups))
4042+
4043+ // for each group, check that it exists and record its id.
4044+ for i, group := range groups {
4045+ found := false
4046+ for _, g := range groupsResp.Groups {
4047+ if g.Name == group.Name {
4048+ groups[i].Id = g.Id
4049+ found = true
4050+ break
4051+ }
4052+ }
4053+ if !found {
4054+ c.Fatalf("group %q not found", group.Name)
4055+ }
4056+ }
4057+
4058+ // check that each instance is part of the correct groups.
4059+ resp, err := ec2conn.Instances([]string{inst0.Id(), inst1.Id()}, nil)
4060+ c.Assert(err, IsNil)
4061+ c.Assert(len(resp.Reservations), Equals, 2, Bug("reservations %#v", resp.Reservations))
4062+ for _, r := range resp.Reservations {
4063+ c.Assert(len(r.Instances), Equals, 1)
4064+ // each instance must be part of the general juju group.
4065+ msg := Bug("reservation %#v", r)
4066+ c.Assert(hasSecurityGroup(r, groups[0]), Equals, true, msg)
4067+ inst := r.Instances[0]
4068+ switch inst.InstanceId {
4069+ case inst0.Id():
4070+ c.Assert(hasSecurityGroup(r, groups[1]), Equals, true, msg)
4071+ c.Assert(hasSecurityGroup(r, groups[2]), Equals, false, msg)
4072+ case inst1.Id():
4073+ c.Assert(hasSecurityGroup(r, groups[2]), Equals, true, msg)
4074+
4075+ // check that the id of the second machine's group
4076+ // has changed - this implies that StartInstance has
4077+ // correctly deleted and re-created the group.
4078+ c.Assert(groups[2].Id, Not(Equals), oldGroup.Id)
4079+ c.Assert(hasSecurityGroup(r, groups[1]), Equals, false, msg)
4080+ default:
4081+ c.Errorf("unknown instance found: %v", inst)
4082+ }
4083+ }
4084+}
4085+
4086+// createGroup creates a new EC2 group if it doesn't already
4087+// exist, and returns full SecurityGroup.
4088+func ensureGroupExists(c *C, ec2conn *amzec2.EC2, group amzec2.SecurityGroup, descr string) amzec2.SecurityGroup {
4089+ groups, err := ec2conn.SecurityGroups([]amzec2.SecurityGroup{group}, nil)
4090+ c.Assert(err, IsNil)
4091+ if len(groups.Groups) > 0 {
4092+ return groups.Groups[0].SecurityGroup
4093+ }
4094+
4095+ resp, err := ec2conn.CreateSecurityGroup(group.Name, descr)
4096+ c.Assert(err, IsNil)
4097+
4098+ return resp.SecurityGroup
4099+}
4100+
4101+func hasSecurityGroup(r amzec2.Reservation, g amzec2.SecurityGroup) bool {
4102+ for _, rg := range r.SecurityGroups {
4103+ if rg.Id == g.Id {
4104+ return true
4105+ }
4106+ }
4107+ return false
4108+}
4109+
4110+func (t *localTests) SetUpTest(c *C) {
4111+ t.srv.startServer(c)
4112+ t.Tests.SetUpTest(c)
4113+}
4114+
4115+func (t *localTests) TearDownTest(c *C) {
4116+ t.Tests.TearDownTest(c)
4117+ t.srv.stopServer(c)
4118+}
4119+
4120+func (t *localLiveTests) SetUpSuite(c *C) {
4121+ t.srv.startServer(c)
4122+ t.LiveTests.SetUpSuite(c)
4123+}
4124+
4125+func (t *localLiveTests) TearDownSuite(c *C) {
4126+ t.srv.stopServer(c)
4127+ t.LiveTests.TearDownSuite(c)
4128+}
4129+
4130+func (srv *localServer) startServer(c *C) {
4131+ var err error
4132+ srv.srv, err = ec2test.NewServer()
4133+ if err != nil {
4134+ c.Fatalf("cannot start ec2 test server: %v", err)
4135+ }
4136+ ec2.Regions["test"] = aws.Region{
4137+ EC2Endpoint: srv.srv.Address(),
4138+ }
4139+ srv.setup(srv.srv)
4140+}
4141+
4142+func (srv *localServer) stopServer(c *C) {
4143+ srv.srv.Quit()
4144+ // Clear out the region because the server address is
4145+ // no longer valid.
4146+ ec2.Regions["test"] = aws.Region{}
4147+}
4148
4149=== added file 'environs/ec2/suite_test.go'
4150--- environs/ec2/suite_test.go 1970-01-01 00:00:00 +0000
4151+++ environs/ec2/suite_test.go 2012-01-18 17:30:30 +0000
4152@@ -0,0 +1,25 @@
4153+package ec2_test
4154+
4155+import (
4156+ "flag"
4157+ . "launchpad.net/gocheck"
4158+ "testing"
4159+)
4160+
4161+type suite struct{}
4162+
4163+var _ = Suite(suite{})
4164+
4165+var regenerate = flag.Bool("regenerate-images", false, "regenerate all data in images directory")
4166+var integration = flag.Bool("i", false, "Enable integration tests")
4167+
4168+func TestEC2(t *testing.T) {
4169+ if *regenerate {
4170+ regenerateImages(t)
4171+ }
4172+ if *integration {
4173+ registerIntegrationTests()
4174+ }
4175+ registerLocalTests()
4176+ TestingT(t)
4177+}
4178
4179=== added file 'environs/ec2/util.go'
4180--- environs/ec2/util.go 1970-01-01 00:00:00 +0000
4181+++ environs/ec2/util.go 2012-01-18 17:30:30 +0000
4182@@ -0,0 +1,43 @@
4183+package ec2
4184+
4185+import (
4186+ "launchpad.net/juju/go/schema"
4187+)
4188+
4189+// this stuff could/should be in the schema package.
4190+
4191+// checkerFunc defines a schema.Checker using a function that
4192+// implemenets scheme.Checker.Coerce.
4193+type checkerFunc func(v interface{}, path []string) (newv interface{}, err error)
4194+
4195+func (f checkerFunc) Coerce(v interface{}, path []string) (newv interface{}, err error) {
4196+ return f(v, path)
4197+}
4198+
4199+// combineCheckers returns a Checker that checks a value by passing
4200+// it through the "pipeline" defined by checkers. When
4201+// the returned checker's Coerce method is called on a value,
4202+// the value is passed through the first checker in checkers;
4203+// the resulting value is used as input to the next checker, and so on.
4204+func combineCheckers(checkers ...schema.Checker) schema.Checker {
4205+ f := func(v interface{}, path []string) (newv interface{}, err error) {
4206+ for _, c := range checkers {
4207+ v, err = c.Coerce(v, path)
4208+ if err != nil {
4209+ return nil, err
4210+ }
4211+ }
4212+ return v, nil
4213+ }
4214+ return checkerFunc(f)
4215+}
4216+
4217+// oneOf(a, b, c) is equivalent to (but less verbose than):
4218+// schema.OneOf(schema.Const(a), schema.Const(b), schema.Const(c))
4219+func oneOf(values ...interface{}) schema.Checker {
4220+ c := make([]schema.Checker, len(values))
4221+ for i, v := range values {
4222+ c[i] = schema.Const(v)
4223+ }
4224+ return schema.OneOf(c...)
4225+}
4226
4227=== added file 'environs/interface.go'
4228--- environs/interface.go 1970-01-01 00:00:00 +0000
4229+++ environs/interface.go 2012-01-18 17:30:30 +0000
4230@@ -0,0 +1,42 @@
4231+package environs
4232+
4233+import "launchpad.net/juju/go/schema"
4234+
4235+// A EnvironProvider represents a computing and storage provider.
4236+type EnvironProvider interface {
4237+ // ConfigChecker is used to check sections of the environments.yaml
4238+ // file that specify this provider. The value passed to the Checker is
4239+ // that returned from the yaml parse, of type schema.MapType.
4240+ ConfigChecker() schema.Checker
4241+
4242+ // NewEnviron creates a new Environ with
4243+ // the given attributes returned by the ConfigChecker.
4244+ // The name is that given in environments.yaml.
4245+ Open(name string, attributes interface{}) (Environ, error)
4246+}
4247+
4248+// Instance represents the provider-specific notion of a machine.
4249+type Instance interface {
4250+ // Id returns a provider-generated identifier for the Instance.
4251+ Id() string
4252+ DNSName() string
4253+}
4254+
4255+// An Environ represents a juju environment as specified
4256+// in the environments.yaml file.
4257+type Environ interface {
4258+ // StartInstance asks for a new instance to be created,
4259+ // associated with the provided machine identifier
4260+ // TODO add arguments to specify type of new machine.
4261+ StartInstance(machineId int) (Instance, error)
4262+
4263+ // StopInstances shuts down the given instances.
4264+ StopInstances([]Instance) error
4265+
4266+ // Instances returns the list of currently started instances.
4267+ Instances() ([]Instance, error)
4268+
4269+ // Destroy shuts down all known machines and destroys the
4270+ // rest of the environment.
4271+ Destroy() error
4272+}
4273
4274=== added directory 'environs/jujutest'
4275=== added file 'environs/jujutest/Makefile'
4276--- environs/jujutest/Makefile 1970-01-01 00:00:00 +0000
4277+++ environs/jujutest/Makefile 2012-01-18 17:30:30 +0000
4278@@ -0,0 +1,25 @@
4279+include $(GOROOT)/src/Make.inc
4280+
4281+all: package
4282+
4283+TARG=launchpad.net/juju/go/environs/jujutest
4284+
4285+GOFILES=\
4286+ test.go\
4287+ tests.go\
4288+ livetests.go\
4289+
4290+GOFMT=gofmt
4291+BADFMT:=$(shell $(GOFMT) -l $(GOFILES) $(CGOFILES) $(wildcard *_test.go))
4292+
4293+gofmt: $(BADFMT)
4294+ @for F in $(BADFMT); do $(GOFMT) -w $$F && echo $$F; done
4295+
4296+ifneq ($(BADFMT),)
4297+ifneq ($(MAKECMDGOALS),gofmt)
4298+$(warning WARNING: make gofmt: $(BADFMT))
4299+endif
4300+endif
4301+
4302+include $(GOROOT)/src/Make.pkg
4303+
4304
4305=== added file 'environs/jujutest/jujutest_test.go'
4306--- environs/jujutest/jujutest_test.go 1970-01-01 00:00:00 +0000
4307+++ environs/jujutest/jujutest_test.go 2012-01-18 17:30:30 +0000
4308@@ -0,0 +1,8 @@
4309+package jujutest
4310+
4311+import "testing"
4312+
4313+// A dummy test so that gotest succeeds when running
4314+// in this directory.
4315+func TestNothing(t *testing.T) {
4316+}
4317
4318=== added file 'environs/jujutest/livetests.go'
4319--- environs/jujutest/livetests.go 1970-01-01 00:00:00 +0000
4320+++ environs/jujutest/livetests.go 2012-01-18 17:30:30 +0000
4321@@ -0,0 +1,52 @@
4322+package jujutest
4323+
4324+import (
4325+ . "launchpad.net/gocheck"
4326+ "launchpad.net/juju/go/environs"
4327+)
4328+
4329+// TestStartStop is similar to Tests.TestStartStop except
4330+// that it does not assume a pristine environment.
4331+func (t *LiveTests) TestStartStop(c *C) {
4332+ names := make(map[string]environs.Instance)
4333+ insts, err := t.env.Instances()
4334+ c.Assert(err, IsNil)
4335+
4336+ // check there are no duplicate instance ids
4337+ for _, inst := range insts {
4338+ id := inst.Id()
4339+ c.Assert(names[id], IsNil)
4340+ names[id] = inst
4341+ }
4342+
4343+ inst, err := t.env.StartInstance(0)
4344+ c.Assert(err, IsNil)
4345+ c.Assert(inst, NotNil)
4346+ id0 := inst.Id()
4347+
4348+ insts, err = t.env.Instances()
4349+ c.Assert(err, IsNil)
4350+
4351+ // check the new instance is found
4352+ found := false
4353+ for _, inst := range insts {
4354+ if inst.Id() == id0 {
4355+ c.Assert(found, Equals, false)
4356+ found = true
4357+ }
4358+ }
4359+ c.Check(found, Equals, true)
4360+
4361+ err = t.env.StopInstances([]environs.Instance{inst})
4362+ c.Assert(err, IsNil)
4363+
4364+ insts, err = t.env.Instances()
4365+ c.Assert(err, IsNil)
4366+ c.Assert(len(insts), Equals, 0)
4367+
4368+ // check the instance is no longer there.
4369+ found = true
4370+ for _, inst := range insts {
4371+ c.Assert(inst.Id(), Not(Equals), id0)
4372+ }
4373+}
4374
4375=== added file 'environs/jujutest/test.go'
4376--- environs/jujutest/test.go 1970-01-01 00:00:00 +0000
4377+++ environs/jujutest/test.go 2012-01-18 17:30:30 +0000
4378@@ -0,0 +1,66 @@
4379+package jujutest
4380+
4381+import (
4382+ . "launchpad.net/gocheck"
4383+ "launchpad.net/juju/go/environs"
4384+)
4385+
4386+// Tests is a gocheck suite containing tests verifying
4387+// juju functionality against the environment with Name that
4388+// must exist within Environs.
4389+type Tests struct {
4390+ Environs *environs.Environs
4391+ Name string
4392+
4393+ environs []environs.Environ
4394+}
4395+
4396+func (t *Tests) open(c *C) environs.Environ {
4397+ e, err := t.Environs.Open(t.Name)
4398+ c.Assert(err, IsNil, Bug("opening environ %q", t.Name))
4399+ c.Assert(e, NotNil)
4400+ t.environs = append(t.environs, e)
4401+ return e
4402+}
4403+
4404+func (t *Tests) SetUpSuite(*C) {
4405+}
4406+
4407+func (t *Tests) TearDownSuite(*C) {
4408+}
4409+
4410+func (t *Tests) SetUpTest(*C) {
4411+}
4412+
4413+func (t *Tests) TearDownTest(c *C) {
4414+ for _, e := range t.environs {
4415+ err := e.Destroy()
4416+ if err != nil {
4417+ c.Errorf("error destroying environment after test: %v", err)
4418+ }
4419+ }
4420+ t.environs = nil
4421+}
4422+
4423+type LiveTests struct {
4424+ Environs *environs.Environs
4425+ Name string
4426+ env environs.Environ
4427+}
4428+
4429+func (t *LiveTests) SetUpSuite(c *C) {
4430+ e, err := t.Environs.Open(t.Name)
4431+ c.Assert(err, IsNil, Bug("opening environ %q", t.Name))
4432+ c.Assert(e, NotNil)
4433+ t.env = e
4434+}
4435+
4436+func (t *LiveTests) TearDownSuite(c *C) {
4437+ t.env = nil
4438+}
4439+
4440+func (t *LiveTests) SetUpTest(*C) {
4441+}
4442+
4443+func (t *LiveTests) TearDownTest(*C) {
4444+}
4445
4446=== added file 'environs/jujutest/tests.go'
4447--- environs/jujutest/tests.go 1970-01-01 00:00:00 +0000
4448+++ environs/jujutest/tests.go 2012-01-18 17:30:30 +0000
4449@@ -0,0 +1,31 @@
4450+package jujutest
4451+
4452+import (
4453+ . "launchpad.net/gocheck"
4454+ "launchpad.net/juju/go/environs"
4455+)
4456+
4457+func (t *Tests) TestStartStop(c *C) {
4458+ e := t.open(c)
4459+
4460+ insts, err := e.Instances()
4461+ c.Assert(err, IsNil)
4462+ c.Assert(len(insts), Equals, 0)
4463+
4464+ inst, err := e.StartInstance(0)
4465+ c.Assert(err, IsNil)
4466+ c.Assert(inst, NotNil)
4467+ id0 := inst.Id()
4468+
4469+ insts, err = e.Instances()
4470+ c.Assert(err, IsNil)
4471+ c.Assert(len(insts), Equals, 1)
4472+ c.Assert(insts[0].Id(), Equals, id0)
4473+
4474+ err = e.StopInstances([]environs.Instance{inst})
4475+ c.Assert(err, IsNil)
4476+
4477+ insts, err = e.Instances()
4478+ c.Assert(err, IsNil)
4479+ c.Assert(len(insts), Equals, 0)
4480+}
4481
4482=== added file 'environs/open.go'
4483--- environs/open.go 1970-01-01 00:00:00 +0000
4484+++ environs/open.go 2012-01-18 17:30:30 +0000
4485@@ -0,0 +1,28 @@
4486+package environs
4487+
4488+import "fmt"
4489+
4490+// New creates a new Environ using the
4491+// environment configuration with the given name.
4492+// If name is empty, the default environment will be used.
4493+func (envs *Environs) Open(name string) (Environ, error) {
4494+ if name == "" {
4495+ name = envs.Default
4496+ if name == "" {
4497+ return nil, fmt.Errorf("no default environment found")
4498+ }
4499+ }
4500+ e, ok := envs.environs[name]
4501+ if !ok {
4502+ return nil, fmt.Errorf("unknown environment %q", name)
4503+ }
4504+ if e.err != nil {
4505+ return nil, e.err
4506+ }
4507+ env, err := providers[e.kind].Open(name, e.config)
4508+ if err != nil {
4509+ return nil, fmt.Errorf("cannot initialize environment %q: %v", name, err)
4510+ }
4511+
4512+ return env, nil
4513+}
4514
4515=== added file 'environs/suite_test.go'
4516--- environs/suite_test.go 1970-01-01 00:00:00 +0000
4517+++ environs/suite_test.go 2012-01-18 17:30:30 +0000
4518@@ -0,0 +1,14 @@
4519+package environs_test
4520+
4521+import (
4522+ . "launchpad.net/gocheck"
4523+ "testing"
4524+)
4525+
4526+func Test(t *testing.T) {
4527+ TestingT(t)
4528+}
4529+
4530+type suite struct{}
4531+
4532+var _ = Suite(suite{})
4533
4534=== added directory 'log'
4535=== added file 'log/Makefile'
4536--- log/Makefile 1970-01-01 00:00:00 +0000
4537+++ log/Makefile 2012-01-18 17:30:30 +0000
4538@@ -0,0 +1,23 @@
4539+include $(GOROOT)/src/Make.inc
4540+
4541+all: package
4542+
4543+TARG=launchpad.net/juju/go/log
4544+
4545+GOFILES=\
4546+ log.go\
4547+
4548+GOFMT=gofmt
4549+BADFMT:=$(shell $(GOFMT) -l $(GOFILES) $(CGOFILES) $(wildcard *_test.go))
4550+
4551+gofmt: $(BADFMT)
4552+ @for F in $(BADFMT); do $(GOFMT) -w $$F && echo $$F; done
4553+
4554+ifneq ($(BADFMT),)
4555+ifneq ($(MAKECMDGOALS),gofmt)
4556+$(warning WARNING: make gofmt: $(BADFMT))
4557+endif
4558+endif
4559+
4560+include $(GOROOT)/src/Make.pkg
4561+
4562
4563=== added file 'log/log.go'
4564--- log/log.go 1970-01-01 00:00:00 +0000
4565+++ log/log.go 2012-01-18 17:30:30 +0000
4566@@ -0,0 +1,27 @@
4567+package log
4568+
4569+import "fmt"
4570+
4571+type Logger interface {
4572+ Output(calldepth int, s string) error
4573+}
4574+
4575+var (
4576+ Target Logger
4577+ Debug bool
4578+)
4579+
4580+// Printf logs the formatted message onto the Target Logger.
4581+func Printf(format string, v ...interface{}) {
4582+ if Target != nil {
4583+ Target.Output(2, "JUJU "+fmt.Sprintf(format, v...))
4584+ }
4585+}
4586+
4587+// Debugf logs the formatted message onto the Target Logger
4588+// if Debug is true.
4589+func Debugf(format string, v ...interface{}) {
4590+ if Debug && Target != nil {
4591+ Target.Output(2, "JUJU:DEBUG "+fmt.Sprintf(format, v...))
4592+ }
4593+}
4594
4595=== added file 'log/log_test.go'
4596--- log/log_test.go 1970-01-01 00:00:00 +0000
4597+++ log/log_test.go 2012-01-18 17:30:30 +0000
4598@@ -0,0 +1,54 @@
4599+package log_test
4600+
4601+import (
4602+ "bytes"
4603+ . "launchpad.net/gocheck"
4604+ "launchpad.net/juju/go/log"
4605+ stdlog "log"
4606+ "testing"
4607+)
4608+
4609+func Test(t *testing.T) {
4610+ TestingT(t)
4611+}
4612+
4613+type suite struct{}
4614+
4615+var _ = Suite(suite{})
4616+
4617+type logTest struct {
4618+ input string
4619+ debug bool
4620+}
4621+
4622+var logTests = []struct {
4623+ input string
4624+ debug bool
4625+}{
4626+ {
4627+ input: "Hello World",
4628+ debug: false,
4629+ },
4630+ {
4631+ input: "Hello World",
4632+ debug: true,
4633+ },
4634+}
4635+
4636+func (suite) TestLogger(c *C) {
4637+ buf := &bytes.Buffer{}
4638+ log.Target = stdlog.New(buf, "", 0)
4639+ for _, t := range logTests {
4640+ log.Debug = t.debug
4641+ log.Printf(t.input)
4642+ c.Assert(buf.String(), Equals, "JUJU "+t.input+"\n")
4643+ buf.Reset()
4644+ log.Debugf(t.input)
4645+ if t.debug {
4646+ c.Assert(buf.String(), Equals, "JUJU:DEBUG "+t.input+"\n")
4647+ } else {
4648+ c.Assert(buf.String(), Equals, "")
4649+ }
4650+ buf.Reset()
4651+ }
4652+}
4653
4654=== added directory 'schema'
4655=== added file 'schema/Makefile'
4656--- schema/Makefile 1970-01-01 00:00:00 +0000
4657+++ schema/Makefile 2012-01-18 17:30:30 +0000
4658@@ -0,0 +1,23 @@
4659+include $(GOROOT)/src/Make.inc
4660+
4661+all: package
4662+
4663+TARG=launchpad.net/juju/go/schema
4664+
4665+GOFILES=\
4666+ schema.go\
4667+
4668+GOFMT=gofmt
4669+BADFMT:=$(shell $(GOFMT) -l $(GOFILES) $(CGOFILES) $(wildcard *_test.go))
4670+
4671+gofmt: $(BADFMT)
4672+ @for F in $(BADFMT); do $(GOFMT) -w $$F && echo $$F; done
4673+
4674+ifneq ($(BADFMT),)
4675+ifneq ($(MAKECMDGOALS),gofmt)
4676+$(warning WARNING: make gofmt: $(BADFMT))
4677+endif
4678+endif
4679+
4680+include $(GOROOT)/src/Make.pkg
4681+
4682
4683=== added file 'schema/schema.go'
4684--- schema/schema.go 1970-01-01 00:00:00 +0000
4685+++ schema/schema.go 2012-01-18 17:30:30 +0000
4686@@ -0,0 +1,374 @@
4687+package schema
4688+
4689+import (
4690+ "fmt"
4691+ "reflect"
4692+ "regexp"
4693+ "strconv"
4694+ "strings"
4695+)
4696+
4697+// All map types used in the schema package are of type MapType.
4698+type MapType map[interface{}]interface{}
4699+
4700+// All the slice types generated in the schema package are of type ListType.
4701+type ListType []interface{}
4702+
4703+// The Coerce method of the Checker interface is called recursively when
4704+// v is being validated. If err is nil, newv is used as the new value
4705+// at the recursion point. If err is non-nil, v is taken as invalid and
4706+// may be either ignored or error out depending on where in the schema
4707+// checking process the error happened. Checkers like OneOf may continue
4708+// with an alternative, for instance.
4709+type Checker interface {
4710+ Coerce(v interface{}, path []string) (newv interface{}, err error)
4711+}
4712+
4713+type error_ struct {
4714+ want string
4715+ got interface{}
4716+ path []string
4717+}
4718+
4719+func (e error_) Error() string {
4720+ var path string
4721+ if e.path[0] == "." {
4722+ path = strings.Join(e.path[1:], "")
4723+ } else {
4724+ path = strings.Join(e.path, "")
4725+ }
4726+ if e.want == "" {
4727+ return fmt.Sprintf("%s: unsupported value", path)
4728+ }
4729+ if e.got == nil {
4730+ return fmt.Sprintf("%s: expected %s, got nothing", path, e.want)
4731+ }
4732+ return fmt.Sprintf("%s: expected %s, got %#v", path, e.want, e.got)
4733+}
4734+
4735+// Any returns a Checker that succeeds with any input value and
4736+// results in the value itself unprocessed.
4737+func Any() Checker {
4738+ return anyC{}
4739+}
4740+
4741+type anyC struct{}
4742+
4743+func (c anyC) Coerce(v interface{}, path []string) (interface{}, error) {
4744+ return v, nil
4745+}
4746+
4747+// Const returns a Checker that only succeeds if the input matches
4748+// value exactly. The value is compared with reflect.DeepEqual.
4749+func Const(value interface{}) Checker {
4750+ return constC{value}
4751+}
4752+
4753+type constC struct {
4754+ value interface{}
4755+}
4756+
4757+func (c constC) Coerce(v interface{}, path []string) (interface{}, error) {
4758+ if reflect.DeepEqual(v, c.value) {
4759+ return v, nil
4760+ }
4761+ return nil, error_{fmt.Sprintf("%#v", c.value), v, path}
4762+}
4763+
4764+// OneOf returns a Checker that attempts to Coerce the value with each
4765+// of the provided checkers. The value returned by the first checker
4766+// that succeeds will be returned by the OneOf checker itself. If no
4767+// checker succeeds, OneOf will return an error on coercion.
4768+func OneOf(options ...Checker) Checker {
4769+ return oneOfC{options}
4770+}
4771+
4772+type oneOfC struct {
4773+ options []Checker
4774+}
4775+
4776+func (c oneOfC) Coerce(v interface{}, path []string) (interface{}, error) {
4777+ for _, o := range c.options {
4778+ newv, err := o.Coerce(v, path)
4779+ if err == nil {
4780+ return newv, nil
4781+ }
4782+ }
4783+ return nil, error_{path: path}
4784+}
4785+
4786+// Bool returns a Checker that accepts boolean values only.
4787+func Bool() Checker {
4788+ return boolC{}
4789+}
4790+
4791+type boolC struct{}
4792+
4793+func (c boolC) Coerce(v interface{}, path []string) (interface{}, error) {
4794+ if v != nil && reflect.TypeOf(v).Kind() == reflect.Bool {
4795+ return v, nil
4796+ }
4797+ return nil, error_{"bool", v, path}
4798+}
4799+
4800+// Int returns a Checker that accepts any integer value, and returns
4801+// the same value consistently typed as an int64.
4802+func Int() Checker {
4803+ return intC{}
4804+}
4805+
4806+type intC struct{}
4807+
4808+func (c intC) Coerce(v interface{}, path []string) (interface{}, error) {
4809+ if v == nil {
4810+ return nil, error_{"int", v, path}
4811+ }
4812+ switch reflect.TypeOf(v).Kind() {
4813+ case reflect.Int:
4814+ case reflect.Int8:
4815+ case reflect.Int16:
4816+ case reflect.Int32:
4817+ case reflect.Int64:
4818+ default:
4819+ return nil, error_{"int", v, path}
4820+ }
4821+ return reflect.ValueOf(v).Int(), nil
4822+}
4823+
4824+// Int returns a Checker that accepts any float value, and returns
4825+// the same value consistently typed as a float64.
4826+func Float() Checker {
4827+ return floatC{}
4828+}
4829+
4830+type floatC struct{}
4831+
4832+func (c floatC) Coerce(v interface{}, path []string) (interface{}, error) {
4833+ if v == nil {
4834+ return nil, error_{"float", v, path}
4835+ }
4836+ switch reflect.TypeOf(v).Kind() {
4837+ case reflect.Float32:
4838+ case reflect.Float64:
4839+ default:
4840+ return nil, error_{"float", v, path}
4841+ }
4842+ return reflect.ValueOf(v).Float(), nil
4843+}
4844+
4845+// String returns a Checker that accepts a string value only and returns
4846+// it unprocessed.
4847+func String() Checker {
4848+ return stringC{}
4849+}
4850+
4851+type stringC struct{}
4852+
4853+func (c stringC) Coerce(v interface{}, path []string) (interface{}, error) {
4854+ if v != nil && reflect.TypeOf(v).Kind() == reflect.String {
4855+ return reflect.ValueOf(v).String(), nil
4856+ }
4857+ return nil, error_{"string", v, path}
4858+}
4859+
4860+func SimpleRegexp() Checker {
4861+ return sregexpC{}
4862+}
4863+
4864+type sregexpC struct{}
4865+
4866+func (c sregexpC) Coerce(v interface{}, path []string) (interface{}, error) {
4867+ // XXX The regexp package happens to be extremely simple right now.
4868+ // Once exp/regexp goes mainstream, we'll have to update this
4869+ // logic to use a more widely accepted regexp subset.
4870+ if v != nil && reflect.TypeOf(v).Kind() == reflect.String {
4871+ s := reflect.ValueOf(v).String()
4872+ _, err := regexp.Compile(s)
4873+ if err != nil {
4874+ return nil, error_{"valid regexp", s, path}
4875+ }
4876+ return v, nil
4877+ }
4878+ return nil, error_{"regexp string", v, path}
4879+}
4880+
4881+// List returns a Checker that accepts a slice value with values
4882+// that are processed with the elem checker. If any element of the
4883+// provided slice value fails to be processed, processing will stop
4884+// and return with the obtained error.
4885+//
4886+// The coerced output value has type schema.ListType.
4887+func List(elem Checker) Checker {
4888+ return listC{elem}
4889+}
4890+
4891+type listC struct {
4892+ elem Checker
4893+}
4894+
4895+func (c listC) Coerce(v interface{}, path []string) (interface{}, error) {
4896+ rv := reflect.ValueOf(v)
4897+ if rv.Kind() != reflect.Slice {
4898+ return nil, error_{"list", v, path}
4899+ }
4900+
4901+ path = append(path, "[", "?", "]")
4902+
4903+ l := rv.Len()
4904+ out := make(ListType, 0, l)
4905+ for i := 0; i != l; i++ {
4906+ path[len(path)-2] = strconv.Itoa(i)
4907+ elem, err := c.elem.Coerce(rv.Index(i).Interface(), path)
4908+ if err != nil {
4909+ return nil, err
4910+ }
4911+ out = append(out, elem)
4912+ }
4913+ return out, nil
4914+}
4915+
4916+// Map returns a Checker that accepts a map value. Every key and value
4917+// in the map are processed with the respective checker, and if any
4918+// value fails to be coerced, processing stops and returns with the
4919+// underlying error.
4920+//
4921+// The coerced output value has type schema.MapType.
4922+func Map(key Checker, value Checker) Checker {
4923+ return mapC{key, value}
4924+}
4925+
4926+type mapC struct {
4927+ key Checker
4928+ value Checker
4929+}
4930+
4931+func (c mapC) Coerce(v interface{}, path []string) (interface{}, error) {
4932+ rv := reflect.ValueOf(v)
4933+ if rv.Kind() != reflect.Map {
4934+ return nil, error_{"map", v, path}
4935+ }
4936+
4937+ vpath := append(path, ".", "?")
4938+
4939+ l := rv.Len()
4940+ out := make(MapType, l)
4941+ keys := rv.MapKeys()
4942+ for i := 0; i != l; i++ {
4943+ k := keys[i]
4944+ newk, err := c.key.Coerce(k.Interface(), path)
4945+ if err != nil {
4946+ return nil, err
4947+ }
4948+ vpath[len(vpath)-1] = fmt.Sprint(k.Interface())
4949+ newv, err := c.value.Coerce(rv.MapIndex(k).Interface(), vpath)
4950+ if err != nil {
4951+ return nil, err
4952+ }
4953+ out[newk] = newv
4954+ }
4955+ return out, nil
4956+}
4957+
4958+type Fields map[string]Checker
4959+type Optional []string
4960+
4961+// FieldMap returns a Checker that accepts a map value with defined
4962+// string keys. Every key has an independent checker associated,
4963+// and processing will only succeed if all the values succeed
4964+// individually. If a field fails to be processed, processing stops
4965+// and returns with the underlying error.
4966+//
4967+// The coerced output value has type schema.MapType.
4968+func FieldMap(fields Fields, optional Optional) Checker {
4969+ return fieldMapC{fields, optional}
4970+}
4971+
4972+type fieldMapC struct {
4973+ fields Fields
4974+ optional []string
4975+}
4976+
4977+func (c fieldMapC) isOptional(key string) bool {
4978+ for _, k := range c.optional {
4979+ if k == key {
4980+ return true
4981+ }
4982+ }
4983+ return false
4984+}
4985+
4986+func (c fieldMapC) Coerce(v interface{}, path []string) (interface{}, error) {
4987+ rv := reflect.ValueOf(v)
4988+ if rv.Kind() != reflect.Map {
4989+ return nil, error_{"map", v, path}
4990+ }
4991+
4992+ vpath := append(path, ".", "?")
4993+
4994+ l := rv.Len()
4995+ out := make(MapType, l)
4996+ for k, checker := range c.fields {
4997+ vpath[len(vpath)-1] = k
4998+ var value interface{}
4999+ valuev := rv.MapIndex(reflect.ValueOf(k))
5000+ if valuev.IsValid() {
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to status/vote changes: