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
=== added file '.bzrignore'
--- .bzrignore 1970-01-01 00:00:00 +0000
+++ .bzrignore 2012-01-18 17:30:30 +0000
@@ -0,0 +1,5 @@
1*.6
2_obj
3_test
46.out
5_testmain.go
06
=== renamed file '.bzrignore' => '.bzrignore.moved'
=== added file '.lbox'
--- .lbox 1970-01-01 00:00:00 +0000
+++ .lbox 2012-01-18 17:30:30 +0000
@@ -0,0 +1,1 @@
1propose -cr -for lp:juju/go
02
=== added directory 'charm'
=== added file 'charm/Makefile'
--- charm/Makefile 1970-01-01 00:00:00 +0000
+++ charm/Makefile 2012-01-18 17:30:30 +0000
@@ -0,0 +1,28 @@
1include $(GOROOT)/src/Make.inc
2
3all: package
4
5TARG=launchpad.net/juju/go/charm
6
7GOFILES=\
8 bundle.go\
9 config.go\
10 dir.go\
11 charm.go\
12 meta.go\
13 url.go\
14
15GOFMT=gofmt
16BADFMT:=$(shell $(GOFMT) -l $(GOFILES) $(CGOFILES) $(wildcard *_test.go))
17
18gofmt: $(BADFMT)
19 @for F in $(BADFMT); do $(GOFMT) -w $$F && echo $$F; done
20
21ifneq ($(BADFMT),)
22ifneq ($(MAKECMDGOALS),gofmt)
23$(warning WARNING: make gofmt: $(BADFMT))
24endif
25endif
26
27include $(GOROOT)/src/Make.pkg
28
029
=== added file 'charm/bundle.go'
--- charm/bundle.go 1970-01-01 00:00:00 +0000
+++ charm/bundle.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,222 @@
1package charm
2
3import (
4 "archive/zip"
5 "errors"
6 "fmt"
7 "io"
8 "os"
9 "path/filepath"
10 "strconv"
11 "strings"
12)
13
14// The Bundle type encapsulates access to data and operations
15// on a charm bundle.
16type Bundle struct {
17 Path string // May be empty if Bundle wasn't read from a file
18 meta *Meta
19 config *Config
20 revision int
21 r io.ReaderAt
22 size int64
23}
24
25// Trick to ensure *Bundle implements the Charm interface.
26var _ Charm = (*Bundle)(nil)
27
28// ReadBundle returns a Bundle for the charm in path.
29func ReadBundle(path string) (bundle *Bundle, err error) {
30 f, err := os.Open(path)
31 if err != nil {
32 return
33 }
34 defer f.Close()
35 fi, err := f.Stat()
36 if err != nil {
37 return
38 }
39 b, err := readBundle(f, fi.Size())
40 if err != nil {
41 return
42 }
43 b.Path = path
44 return b, nil
45}
46
47// ReadBundleBytes returns a Bundle read from the given data.
48// Make sure the bundle fits in memory before using this.
49func ReadBundleBytes(data []byte) (bundle *Bundle, err error) {
50 return readBundle(readAtBytes(data), int64(len(data)))
51}
52
53func readBundle(r io.ReaderAt, size int64) (bundle *Bundle, err error) {
54 b := &Bundle{r: r, size: size}
55 zipr, err := zip.NewReader(r, size)
56 if err != nil {
57 return
58 }
59
60 reader, err := zipOpen(zipr, "metadata.yaml")
61 if err != nil {
62 return
63 }
64 b.meta, err = ReadMeta(reader)
65 reader.Close()
66 if err != nil {
67 return
68 }
69
70 reader, err = zipOpen(zipr, "config.yaml")
71 if err != nil {
72 return
73 }
74 b.config, err = ReadConfig(reader)
75 reader.Close()
76 if err != nil {
77 return
78 }
79
80 reader, err = zipOpen(zipr, "revision")
81 if err != nil {
82 if _, ok := err.(noBundleFile); !ok {
83 return
84 }
85 b.revision = b.meta.OldRevision
86 } else {
87 _, err = fmt.Fscan(reader, &b.revision)
88 if err != nil {
89 return nil, errors.New("invalid revision file")
90 }
91 }
92 return b, nil
93}
94
95func zipOpen(zipr *zip.Reader, path string) (rc io.ReadCloser, err error) {
96 for _, fh := range zipr.File {
97 if fh.Name == path {
98 return fh.Open()
99 }
100 }
101 return nil, noBundleFile{path}
102}
103
104type noBundleFile struct {
105 path string
106}
107
108func (err noBundleFile) Error() string {
109 return fmt.Sprintf("bundle file not found: %s", err.path)
110}
111
112// Revision returns the revision number for the charm
113// expanded in dir.
114func (b *Bundle) Revision() int {
115 return b.revision
116}
117
118// SetRevision changes the charm revision number. This affects the
119// revision reported by Revision and the revision of the charm
120// directory created by ExpandTo.
121func (b *Bundle) SetRevision(revision int) {
122 b.revision = revision
123}
124
125// Meta returns the Meta representing the metadata.yaml file from bundle.
126func (b *Bundle) Meta() *Meta {
127 return b.meta
128}
129
130// Config returns the Config representing the config.yaml file
131// for the charm bundle.
132func (b *Bundle) Config() *Config {
133 return b.config
134}
135
136// ExpandTo expands the charm bundle into dir, creating it if necessary.
137// If any errors occur during the expansion procedure, the process will
138// continue. Only the last error found is returned.
139func (b *Bundle) ExpandTo(dir string) (err error) {
140 // If we have a Path, reopen the file. Otherwise, try to use
141 // the original ReaderAt.
142 r := b.r
143 size := b.size
144 if b.Path != "" {
145 f, err := os.Open(b.Path)
146 if err != nil {
147 return err
148 }
149 defer f.Close()
150 fi, err := f.Stat()
151 if err != nil {
152 return err
153 }
154 r = f
155 size = fi.Size()
156 }
157
158 zipr, err := zip.NewReader(r, size)
159 if err != nil {
160 return err
161 }
162
163 var lasterr error
164 for _, zfile := range zipr.File {
165 if err := b.expand(dir, zfile); err != nil {
166 lasterr = err
167 }
168 }
169
170 revFile, err := os.Create(filepath.Join(dir, "revision"))
171 if err != nil {
172 return err
173 }
174 _, err = revFile.Write([]byte(strconv.Itoa(b.revision)))
175 revFile.Close()
176 if err != nil {
177 return err
178 }
179 return lasterr
180}
181
182func (b *Bundle) expand(dir string, zfile *zip.File) error {
183 cleanName := filepath.Clean(zfile.Name)
184 if cleanName == "revision" {
185 return nil
186 }
187
188 r, err := zfile.Open()
189 if err != nil {
190 return err
191 }
192 defer r.Close()
193
194 path := filepath.Join(dir, cleanName)
195 if strings.HasSuffix(zfile.Name, "/") {
196 err = os.MkdirAll(path, 0755)
197 if err != nil {
198 return err
199 }
200 return nil
201 }
202
203 base, _ := filepath.Split(path)
204 err = os.MkdirAll(base, 0755)
205 if err != nil {
206 return err
207 }
208 f, err := os.Create(path)
209 if err != nil {
210 return err
211 }
212 _, err = io.Copy(f, r)
213 f.Close()
214 return err
215}
216
217// FWIW, being able to do this is awesome.
218type readAtBytes []byte
219
220func (b readAtBytes) ReadAt(out []byte, off int64) (n int, err error) {
221 return copy(out, b[off:]), nil
222}
0223
=== added file 'charm/bundle_test.go'
--- charm/bundle_test.go 1970-01-01 00:00:00 +0000
+++ charm/bundle_test.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,122 @@
1package charm_test
2
3import (
4 "fmt"
5 "io/ioutil"
6 . "launchpad.net/gocheck"
7 "launchpad.net/juju/go/charm"
8 "os"
9 "os/exec"
10 "path/filepath"
11)
12
13type BundleSuite struct {
14 bundlePath string
15}
16
17var _ = Suite(&BundleSuite{})
18
19func (s *BundleSuite) SetUpSuite(c *C) {
20 s.bundlePath = bundleDir(c, repoDir("dummy"))
21}
22
23func (s *BundleSuite) TestReadBundle(c *C) {
24 bundle, err := charm.ReadBundle(s.bundlePath)
25 c.Assert(err, IsNil)
26 checkDummy(c, bundle, s.bundlePath)
27}
28
29func (s *BundleSuite) TestReadBundleBytes(c *C) {
30 data, err := ioutil.ReadFile(s.bundlePath)
31 c.Assert(err, IsNil)
32
33 bundle, err := charm.ReadBundleBytes(data)
34 c.Assert(err, IsNil)
35 checkDummy(c, bundle, "")
36}
37
38func (s *BundleSuite) TestExpandTo(c *C) {
39 bundle, err := charm.ReadBundle(s.bundlePath)
40 c.Assert(err, IsNil)
41
42 path := filepath.Join(c.MkDir(), "charm")
43 err = bundle.ExpandTo(path)
44 c.Assert(err, IsNil)
45
46 dir, err := charm.ReadDir(path)
47 c.Assert(err, IsNil)
48 checkDummy(c, dir, path)
49}
50
51func (s *BundleSuite) TestBundleRevisionFile(c *C) {
52 charmDir := c.MkDir()
53 copyCharmDir(charmDir, repoDir("dummy"))
54 revPath := filepath.Join(charmDir, "revision")
55
56 // Missing revision file
57 err := os.Remove(revPath)
58 c.Assert(err, IsNil)
59
60 bundle, err := charm.ReadBundle(extBundleDir(c, charmDir))
61 c.Assert(err, IsNil)
62 c.Assert(bundle.Revision(), Equals, 0)
63
64 // Missing revision file with old revision in metadata
65 file, err := os.OpenFile(filepath.Join(charmDir, "metadata.yaml"), os.O_WRONLY|os.O_APPEND, 0)
66 c.Assert(err, IsNil)
67 _, err = file.Write([]byte("\nrevision: 1234\n"))
68 c.Assert(err, IsNil)
69
70 bundle, err = charm.ReadBundle(extBundleDir(c, charmDir))
71 c.Assert(err, IsNil)
72 c.Assert(bundle.Revision(), Equals, 1234)
73
74 // Revision file with bad content
75 err = ioutil.WriteFile(revPath, []byte("garbage"), 0666)
76 c.Assert(err, IsNil)
77
78 bundle, err = charm.ReadBundle(extBundleDir(c, charmDir))
79 c.Assert(err, ErrorMatches, "invalid revision file")
80 c.Assert(bundle, IsNil)
81}
82
83func (s *BundleSuite) TestBundleSetRevision(c *C) {
84 bundle, err := charm.ReadBundle(s.bundlePath)
85 c.Assert(err, IsNil)
86
87 c.Assert(bundle.Revision(), Equals, 1)
88 bundle.SetRevision(42)
89 c.Assert(bundle.Revision(), Equals, 42)
90
91 path := filepath.Join(c.MkDir(), "charm")
92 err = bundle.ExpandTo(path)
93 c.Assert(err, IsNil)
94
95 dir, err := charm.ReadDir(path)
96 c.Assert(err, IsNil)
97 c.Assert(dir.Revision(), Equals, 42)
98}
99
100func bundleDir(c *C, dirpath string) (path string) {
101 dir, err := charm.ReadDir(dirpath)
102 c.Assert(err, IsNil)
103
104 path = filepath.Join(c.MkDir(), "bundle.charm")
105
106 file, err := os.Create(path)
107 c.Assert(err, IsNil)
108
109 err = dir.BundleTo(file)
110 c.Assert(err, IsNil)
111 file.Close()
112
113 return path
114}
115
116func extBundleDir(c *C, dirpath string) (path string) {
117 path = filepath.Join(c.MkDir(), "bundle.charm")
118 cmd := exec.Command("/bin/sh", "-c", fmt.Sprintf("cd %s; zip -r %s .", dirpath, path))
119 output, err := cmd.CombinedOutput()
120 c.Assert(err, IsNil, Bug("Command output: %s", output))
121 return path
122}
0123
=== added file 'charm/charm.go'
--- charm/charm.go 1970-01-01 00:00:00 +0000
+++ charm/charm.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,9 @@
1package charm
2
3// The Charm interface is implemented by any type that
4// may be handled as a charm.
5type Charm interface {
6 Meta() *Meta
7 Config() *Config
8 Revision() int
9}
010
=== added file 'charm/charm_test.go'
--- charm/charm_test.go 1970-01-01 00:00:00 +0000
+++ charm/charm_test.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,58 @@
1package charm_test
2
3import (
4 "bytes"
5 "io"
6 "io/ioutil"
7 . "launchpad.net/gocheck"
8 "launchpad.net/goyaml"
9 "launchpad.net/juju/go/charm"
10 "os"
11 "path/filepath"
12 "testing"
13)
14
15func Test(t *testing.T) {
16 TestingT(t)
17}
18
19type S struct{}
20
21var _ = Suite(&S{})
22
23func checkDummy(c *C, f charm.Charm, path string) {
24 c.Assert(f.Revision(), Equals, 1)
25 c.Assert(f.Meta().Name, Equals, "dummy")
26 c.Assert(f.Config().Options["title"].Default, Equals, "My Title")
27 switch f := f.(type) {
28 case *charm.Bundle:
29 c.Assert(f.Path, Equals, path)
30 case *charm.Dir:
31 c.Assert(f.Path, Equals, path)
32 _, err := os.Stat(filepath.Join(path, "src", "hello.c"))
33 c.Assert(err, IsNil)
34 }
35}
36
37type YamlHacker map[interface{}]interface{}
38
39func ReadYaml(r io.Reader) YamlHacker {
40 data, err := ioutil.ReadAll(r)
41 if err != nil {
42 panic(err)
43 }
44 m := make(map[interface{}]interface{})
45 err = goyaml.Unmarshal(data, m)
46 if err != nil {
47 panic(err)
48 }
49 return YamlHacker(m)
50}
51
52func (yh YamlHacker) Reader() io.Reader {
53 data, err := goyaml.Marshal(yh)
54 if err != nil {
55 panic(err)
56 }
57 return bytes.NewBuffer(data)
58}
059
=== added file 'charm/config.go'
--- charm/config.go 1970-01-01 00:00:00 +0000
+++ charm/config.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,117 @@
1package charm
2
3import (
4 "errors"
5 "fmt"
6 "io"
7 "io/ioutil"
8 "launchpad.net/goyaml"
9 "launchpad.net/juju/go/schema"
10 "strconv"
11)
12
13// Option represents a single configuration option that is declared
14// as supported by a charm in its config.yaml file.
15type Option struct {
16 Title string
17 Description string
18 Type string
19 Default interface{}
20}
21
22// Config represents the supported configuration options for a charm,
23// as declared in its config.yaml file.
24type Config struct {
25 Options map[string]Option
26}
27
28// ReadConfig reads a config.yaml file and returns its representation.
29func ReadConfig(r io.Reader) (config *Config, err error) {
30 data, err := ioutil.ReadAll(r)
31 if err != nil {
32 return
33 }
34 raw := make(map[interface{}]interface{})
35 err = goyaml.Unmarshal(data, raw)
36 if err != nil {
37 return
38 }
39 v, err := configSchema.Coerce(raw, nil)
40 if err != nil {
41 return nil, errors.New("config: " + err.Error())
42 }
43 config = &Config{}
44 config.Options = make(map[string]Option)
45 m := v.(schema.MapType)
46 for name, infov := range m["options"].(schema.MapType) {
47 opt := infov.(schema.MapType)
48 optTitle, _ := opt["title"].(string)
49 optType, _ := opt["type"].(string)
50 optDescr, _ := opt["description"].(string)
51 optDefault, _ := opt["default"]
52 config.Options[name.(string)] = Option{
53 Title: optTitle,
54 Type: optType,
55 Description: optDescr,
56 Default: optDefault,
57 }
58 }
59 return
60}
61
62// Validate processes the values in the input map according to the
63// configuration in config, doing the following operations:
64//
65// - Values are converted from strings to the types defined
66// - Options with default values are introduced for missing keys
67// - Unknown keys and badly typed values are reported as errors
68//
69func (c *Config) Validate(values map[string]string) (processed map[string]interface{}, err error) {
70 out := make(map[string]interface{})
71 for k, v := range values {
72 opt, ok := c.Options[k]
73 if !ok {
74 return nil, fmt.Errorf("Unknown configuration option: %q", k)
75 }
76 switch opt.Type {
77 case "string":
78 out[k] = v
79 case "int":
80 i, err := strconv.ParseInt(v, 10, 64)
81 if err != nil {
82 return nil, fmt.Errorf("Value for %q is not an int: %q", k, v)
83 }
84 out[k] = i
85 case "float":
86 f, err := strconv.ParseFloat(v, 64)
87 if err != nil {
88 return nil, fmt.Errorf("Value for %q is not a float: %q", k, v)
89 }
90 out[k] = f
91 default:
92 panic(fmt.Errorf("Internal error: option type %q is unknown to Validate", opt.Type))
93 }
94 }
95 for k, opt := range c.Options {
96 if _, ok := out[k]; !ok && opt.Default != nil {
97 out[k] = opt.Default
98 }
99 }
100 return out, nil
101}
102
103var optionSchema = schema.FieldMap(
104 schema.Fields{
105 "type": schema.OneOf(schema.Const("string"), schema.Const("int"), schema.Const("float")),
106 "default": schema.OneOf(schema.String(), schema.Int(), schema.Float()),
107 "description": schema.String(),
108 },
109 schema.Optional{"default", "description"},
110)
111
112var configSchema = schema.FieldMap(
113 schema.Fields{
114 "options": schema.Map(schema.String(), optionSchema),
115 },
116 nil,
117)
0118
=== added file 'charm/config_test.go'
--- charm/config_test.go 1970-01-01 00:00:00 +0000
+++ charm/config_test.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,143 @@
1package charm_test
2
3import (
4 "bytes"
5 "io"
6 "io/ioutil"
7 . "launchpad.net/gocheck"
8 "launchpad.net/juju/go/charm"
9 "os"
10 "path/filepath"
11)
12
13var sampleConfig = `
14options:
15 title:
16 default: My Title
17 description: A descriptive title used for the service.
18 type: string
19 outlook:
20 description: No default outlook.
21 type: string
22 username:
23 default: admin001
24 description: The name of the initial account (given admin permissions).
25 type: string
26 skill-level:
27 description: A number indicating skill.
28 type: int
29 agility-ratio:
30 description: A number from 0 to 1 indicating agility.
31 type: float
32`
33
34func repoConfig(name string) io.Reader {
35 file, err := os.Open(filepath.Join("testrepo", "series", name, "config.yaml"))
36 if err != nil {
37 panic(err)
38 }
39 defer file.Close()
40 data, err := ioutil.ReadAll(file)
41 if err != nil {
42 panic(err)
43 }
44 return bytes.NewBuffer(data)
45}
46
47func (s *S) TestReadConfig(c *C) {
48 config, err := charm.ReadConfig(repoConfig("dummy"))
49 c.Assert(err, IsNil)
50 c.Assert(config.Options["title"], Equals,
51 charm.Option{
52 Default: "My Title",
53 Description: "A descriptive title used for the service.",
54 Type: "string",
55 },
56 )
57}
58
59func (s *S) TestConfigError(c *C) {
60 _, err := charm.ReadConfig(bytes.NewBuffer([]byte(`options: {t: {type: foo}}`)))
61 c.Assert(err, ErrorMatches, `config: options.t.type: unsupported value`)
62}
63
64func (s *S) TestParseSample(c *C) {
65 config, err := charm.ReadConfig(bytes.NewBuffer([]byte(sampleConfig)))
66 c.Assert(err, IsNil)
67
68 opt := config.Options
69 c.Assert(opt["title"], Equals,
70 charm.Option{
71 Default: "My Title",
72 Description: "A descriptive title used for the service.",
73 Type: "string",
74 },
75 )
76 c.Assert(opt["outlook"], Equals,
77 charm.Option{
78 Description: "No default outlook.",
79 Type: "string",
80 },
81 )
82 c.Assert(opt["username"], Equals,
83 charm.Option{
84 Default: "admin001",
85 Description: "The name of the initial account (given admin permissions).",
86 Type: "string",
87 },
88 )
89 c.Assert(opt["skill-level"], Equals,
90 charm.Option{
91 Description: "A number indicating skill.",
92 Type: "int",
93 },
94 )
95}
96
97func (s *S) TestValidate(c *C) {
98 config, err := charm.ReadConfig(bytes.NewBuffer([]byte(sampleConfig)))
99 c.Assert(err, IsNil)
100
101 input := map[string]string{
102 "title": "Helpful Title",
103 "outlook": "Peachy",
104 }
105
106 // This should include an overridden value, a default and a new value.
107 expected := map[string]interface{}{
108 "title": "Helpful Title",
109 "outlook": "Peachy",
110 "username": "admin001",
111 }
112
113 output, err := config.Validate(input)
114 c.Assert(err, IsNil)
115 c.Assert(output, Equals, expected)
116
117 // Check whether float conversion is working.
118 input["agility-ratio"] = "0.5"
119 input["skill-level"] = "7"
120 expected["agility-ratio"] = 0.5
121 expected["skill-level"] = int64(7)
122 output, err = config.Validate(input)
123 c.Assert(err, IsNil)
124 c.Assert(output, Equals, expected)
125
126 // Check whether float errors are caught.
127 input["agility-ratio"] = "foo"
128 output, err = config.Validate(input)
129 c.Assert(err, ErrorMatches, `Value for "agility-ratio" is not a float: "foo"`)
130 input["agility-ratio"] = "0.5"
131
132 // Check whether int errors are caught.
133 input["skill-level"] = "foo"
134 output, err = config.Validate(input)
135 c.Assert(err, ErrorMatches, `Value for "skill-level" is not an int: "foo"`)
136 input["skill-level"] = "7"
137
138 // Now try to set a value outside the expected.
139 input["bad"] = "value"
140 output, err = config.Validate(input)
141 c.Assert(output, IsNil)
142 c.Assert(err, ErrorMatches, `Unknown configuration option: "bad"`)
143}
0144
=== added file 'charm/dir.go'
--- charm/dir.go 1970-01-01 00:00:00 +0000
+++ charm/dir.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,174 @@
1package charm
2
3import (
4 "archive/zip"
5 "errors"
6 "fmt"
7 "io"
8 "io/ioutil"
9 "os"
10 "path/filepath"
11 "strconv"
12 "syscall"
13)
14
15// The Dir type encapsulates access to data and operations
16// on a charm directory.
17type Dir struct {
18 Path string
19 meta *Meta
20 config *Config
21 revision int
22}
23
24// Trick to ensure *Dir implements the Charm interface.
25var _ Charm = (*Dir)(nil)
26
27// ReadDir returns a Dir representing an expanded charm directory.
28func ReadDir(path string) (dir *Dir, err error) {
29 dir = &Dir{Path: path}
30 file, err := os.Open(dir.join("metadata.yaml"))
31 if err != nil {
32 return nil, err
33 }
34 dir.meta, err = ReadMeta(file)
35 file.Close()
36 if err != nil {
37 return nil, err
38 }
39 file, err = os.Open(dir.join("config.yaml"))
40 if err != nil {
41 return nil, err
42 }
43 dir.config, err = ReadConfig(file)
44 file.Close()
45 if err != nil {
46 return nil, err
47 }
48 if file, err = os.Open(dir.join("revision")); err == nil {
49 _, err = fmt.Fscan(file, &dir.revision)
50 file.Close()
51 if err != nil {
52 return nil, errors.New("invalid revision file")
53 }
54 } else {
55 dir.revision = dir.meta.OldRevision
56 }
57 return dir, nil
58}
59
60// join builds a path rooted at the charm's expanded directory
61// path and the extra path components provided.
62func (dir *Dir) join(parts ...string) string {
63 parts = append([]string{dir.Path}, parts...)
64 return filepath.Join(parts...)
65}
66
67// Revision returns the revision number for the charm
68// expanded in dir.
69func (dir *Dir) Revision() int {
70 return dir.revision
71}
72
73// Meta returns the Meta representing the metadata.yaml file
74// for the charm expanded in dir.
75func (dir *Dir) Meta() *Meta {
76 return dir.meta
77}
78
79// Config returns the Config representing the config.yaml file
80// for the charm expanded in dir.
81func (dir *Dir) Config() *Config {
82 return dir.config
83}
84
85// SetRevision changes the charm revision number. This affects
86// the revision reported by Revision and the revision of the
87// charm bundled by BundleTo.
88// The revision file in the charm directory is not modified.
89func (dir *Dir) SetRevision(revision int) {
90 dir.revision = revision
91}
92
93// SetDiskRevision does the same as SetRevision but also changes
94// the revision file in the charm directory.
95func (dir *Dir) SetDiskRevision(revision int) error {
96 dir.SetRevision(revision)
97 file, err := os.OpenFile(dir.join("revision"), os.O_WRONLY|os.O_CREATE, 0644)
98 if err != nil {
99 return err
100 }
101 _, err = file.Write([]byte(strconv.Itoa(revision)))
102 file.Close()
103 return err
104}
105
106// BundleTo creates a charm file from the charm expanded in dir.
107func (dir *Dir) BundleTo(w io.Writer) (err error) {
108 zipw := zip.NewWriter(w)
109 defer zipw.Close()
110 zp := zipPacker{zipw, dir.Path}
111 zp.AddRevision(dir.revision)
112 return filepath.Walk(dir.Path, zp.WalkFunc())
113}
114
115type zipPacker struct {
116 *zip.Writer
117 root string
118}
119
120func (zp *zipPacker) WalkFunc() filepath.WalkFunc {
121 return func(path string, fi os.FileInfo, err error) error {
122 return zp.visit(path, fi, err)
123 }
124}
125
126func (zp *zipPacker) AddRevision(revision int) error {
127 h := &zip.FileHeader{Name: "revision"}
128 h.SetMode(syscall.S_IFREG | 0644)
129 w, err := zp.CreateHeader(h)
130 if err == nil {
131 _, err = w.Write([]byte(strconv.Itoa(revision)))
132 }
133 return err
134}
135
136func (zp *zipPacker) visit(path string, fi os.FileInfo, err error) error {
137 if err != nil {
138 return err
139 }
140 relpath, err := filepath.Rel(zp.root, path)
141 if err != nil {
142 return err
143 }
144 method := zip.Deflate
145 hidden := len(relpath) > 1 && relpath[0] == '.'
146 if fi.IsDir() {
147 if relpath == "build" {
148 return filepath.SkipDir
149 }
150 if hidden {
151 return filepath.SkipDir
152 }
153 relpath += "/"
154 method = zip.Store
155 }
156 if hidden || relpath == "revision" {
157 return nil
158 }
159 h := &zip.FileHeader{
160 Name: relpath,
161 Method: method,
162 }
163 h.SetMode(fi.Mode())
164 w, err := zp.CreateHeader(h)
165 if err != nil || fi.IsDir() {
166 return err
167 }
168 data, err := ioutil.ReadFile(path)
169 if err != nil {
170 return err
171 }
172 _, err = w.Write(data)
173 return err
174}
0175
=== added file 'charm/dir_test.go'
--- charm/dir_test.go 1970-01-01 00:00:00 +0000
+++ charm/dir_test.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,167 @@
1package charm_test
2
3import (
4 "archive/zip"
5 "bytes"
6 "io/ioutil"
7 . "launchpad.net/gocheck"
8 "launchpad.net/juju/go/charm"
9 "os"
10 "path/filepath"
11)
12
13func repoDir(name string) (path string) {
14 return filepath.Join("testrepo", "series", name)
15}
16
17func (s *S) TestReadDir(c *C) {
18 path := repoDir("dummy")
19 dir, err := charm.ReadDir(path)
20 c.Assert(err, IsNil)
21 checkDummy(c, dir, path)
22}
23
24func (s *S) TestBundleTo(c *C) {
25 dir, err := charm.ReadDir(repoDir("dummy"))
26 c.Assert(err, IsNil)
27
28 path := filepath.Join(c.MkDir(), "bundle.charm")
29 file, err := os.Create(path)
30 c.Assert(err, IsNil)
31 err = dir.BundleTo(file)
32 file.Close()
33 c.Assert(err, IsNil)
34
35 zipr, err := zip.OpenReader(path)
36 c.Assert(err, IsNil)
37 defer zipr.Close()
38
39 var metaf, instf, emptyf, revf *zip.File
40 for _, f := range zipr.File {
41 c.Logf("Bundled file: %s", f.Name)
42 switch f.Name {
43 case "revision":
44 revf = f
45 case "metadata.yaml":
46 metaf = f
47 case "hooks/install":
48 instf = f
49 case "empty/":
50 emptyf = f
51 case "build/ignored":
52 c.Errorf("bundle includes build/*: %s", f.Name)
53 case ".ignored", ".dir/ignored":
54 c.Errorf("bundle includes .* entries: %s", f.Name)
55 }
56 }
57
58 c.Assert(revf, NotNil)
59 reader, err := revf.Open()
60 c.Assert(err, IsNil)
61 data, err := ioutil.ReadAll(reader)
62 reader.Close()
63 c.Assert(err, IsNil)
64 c.Assert(string(data), Equals, "1")
65
66 c.Assert(metaf, NotNil)
67 reader, err = metaf.Open()
68 c.Assert(err, IsNil)
69 meta, err := charm.ReadMeta(reader)
70 reader.Close()
71 c.Assert(err, IsNil)
72 c.Assert(meta.Name, Equals, "dummy")
73
74 c.Assert(instf, NotNil)
75 mode, err := instf.Mode()
76 c.Assert(err, IsNil)
77 c.Assert(mode&0700, Equals, os.FileMode(0700))
78
79 c.Assert(emptyf, NotNil)
80 mode, err = emptyf.Mode()
81 c.Assert(err, IsNil)
82 c.Assert(mode&os.ModeType, Equals, os.ModeDir)
83}
84
85func copyCharmDir(dst, src string) {
86 dir, err := charm.ReadDir(src)
87 if err != nil {
88 panic(err)
89 }
90 var b bytes.Buffer
91 err = dir.BundleTo(&b)
92 if err != nil {
93 panic(err)
94 }
95 bundle, err := charm.ReadBundleBytes(b.Bytes())
96 if err != nil {
97 panic(err)
98 }
99 err = bundle.ExpandTo(dst)
100 if err != nil {
101 panic(err)
102 }
103}
104
105func (s *S) TestDirRevisionFile(c *C) {
106 charmDir := c.MkDir()
107 copyCharmDir(charmDir, repoDir("dummy"))
108 revPath := filepath.Join(charmDir, "revision")
109
110 // Missing revision file
111 err := os.Remove(revPath)
112 c.Assert(err, IsNil)
113
114 dir, err := charm.ReadDir(charmDir)
115 c.Assert(err, IsNil)
116 c.Assert(dir.Revision(), Equals, 0)
117
118 // Missing revision file with old revision in metadata
119 file, err := os.OpenFile(filepath.Join(charmDir, "metadata.yaml"), os.O_WRONLY|os.O_APPEND, 0)
120 c.Assert(err, IsNil)
121 _, err = file.Write([]byte("\nrevision: 1234\n"))
122 c.Assert(err, IsNil)
123
124 dir, err = charm.ReadDir(charmDir)
125 c.Assert(err, IsNil)
126 c.Assert(dir.Revision(), Equals, 1234)
127
128 // Revision file with bad content
129 err = ioutil.WriteFile(revPath, []byte("garbage"), 0666)
130 c.Assert(err, IsNil)
131
132 dir, err = charm.ReadDir(charmDir)
133 c.Assert(err, ErrorMatches, "invalid revision file")
134 c.Assert(dir, IsNil)
135}
136
137func (s *S) TestDirSetRevision(c *C) {
138 dir, err := charm.ReadDir(repoDir("dummy"))
139 c.Assert(err, IsNil)
140
141 c.Assert(dir.Revision(), Equals, 1)
142 dir.SetRevision(42)
143 c.Assert(dir.Revision(), Equals, 42)
144
145 var b bytes.Buffer
146 err = dir.BundleTo(&b)
147 c.Assert(err, IsNil)
148
149 bundle, err := charm.ReadBundleBytes(b.Bytes())
150 c.Assert(bundle.Revision(), Equals, 42)
151}
152
153func (s *S) TestDirSetDiskRevision(c *C) {
154 charmDir := c.MkDir()
155 copyCharmDir(charmDir, repoDir("dummy"))
156
157 dir, err := charm.ReadDir(charmDir)
158 c.Assert(err, IsNil)
159
160 c.Assert(dir.Revision(), Equals, 1)
161 dir.SetDiskRevision(42)
162 c.Assert(dir.Revision(), Equals, 42)
163
164 dir, err = charm.ReadDir(charmDir)
165 c.Assert(err, IsNil)
166 c.Assert(dir.Revision(), Equals, 42)
167}
0168
=== added file 'charm/export_test.go'
--- charm/export_test.go 1970-01-01 00:00:00 +0000
+++ charm/export_test.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,11 @@
1package charm
2
3import (
4 "launchpad.net/juju/go/schema"
5)
6
7// Export meaningful bits for tests only.
8
9func IfaceExpander(limit interface{}) schema.Checker {
10 return ifaceExpander(limit)
11}
012
=== added file 'charm/meta.go'
--- charm/meta.go 1970-01-01 00:00:00 +0000
+++ charm/meta.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,163 @@
1package charm
2
3import (
4 "errors"
5 "io"
6 "io/ioutil"
7 "launchpad.net/goyaml"
8 "launchpad.net/juju/go/schema"
9)
10
11// Relation represents a single relation defined in the charm
12// metadata.yaml file.
13type Relation struct {
14 Interface string
15 Optional bool
16 Limit int
17}
18
19// Meta represents all the known content that may be defined
20// within a charm's metadata.yaml file.
21type Meta struct {
22 Name string
23 Summary string
24 Description string
25 Provides map[string]Relation
26 Requires map[string]Relation
27 Peers map[string]Relation
28 OldRevision int // Obsolete
29}
30
31// ReadMeta reads the content of a metadata.yaml file and returns
32// its representation.
33func ReadMeta(r io.Reader) (meta *Meta, err error) {
34 data, err := ioutil.ReadAll(r)
35 if err != nil {
36 return
37 }
38 raw := make(map[interface{}]interface{})
39 err = goyaml.Unmarshal(data, raw)
40 if err != nil {
41 return
42 }
43 v, err := charmSchema.Coerce(raw, nil)
44 if err != nil {
45 return nil, errors.New("metadata: " + err.Error())
46 }
47 m := v.(schema.MapType)
48 meta = &Meta{}
49 meta.Name = m["name"].(string)
50 // Schema decodes as int64, but the int range should be good
51 // enough for revisions.
52 meta.Summary = m["summary"].(string)
53 meta.Description = m["description"].(string)
54 meta.Provides = parseRelations(m["provides"])
55 meta.Requires = parseRelations(m["requires"])
56 meta.Peers = parseRelations(m["peers"])
57 if rev := m["revision"]; rev != nil {
58 // Obsolete
59 meta.OldRevision = int(m["revision"].(int64))
60 }
61 return
62}
63
64func parseRelations(relations interface{}) map[string]Relation {
65 if relations == nil {
66 return nil
67 }
68 result := make(map[string]Relation)
69 for name, rel := range relations.(schema.MapType) {
70 relMap := rel.(schema.MapType)
71 relation := Relation{}
72 relation.Interface = relMap["interface"].(string)
73 relation.Optional = relMap["optional"].(bool)
74 if relMap["limit"] != nil {
75 // Schema defaults to int64, but we know
76 // the int range should be more than enough.
77 relation.Limit = int(relMap["limit"].(int64))
78 }
79 result[name.(string)] = relation
80 }
81 return result
82}
83
84// Schema coercer that expands the interface shorthand notation.
85// A consistent format is easier to work with than considering the
86// potential difference everywhere.
87//
88// Supports the following variants::
89//
90// provides:
91// server: riak
92// admin: http
93// foobar:
94// interface: blah
95//
96// provides:
97// server:
98// interface: mysql
99// limit:
100// optional: false
101//
102// In all input cases, the output is the fully specified interface
103// representation as seen in the mysql interface description above.
104func ifaceExpander(limit interface{}) schema.Checker {
105 return ifaceExpC{limit}
106}
107
108type ifaceExpC struct {
109 limit interface{}
110}
111
112var (
113 stringC = schema.String()
114 mapC = schema.Map(schema.String(), schema.Any())
115)
116
117func (c ifaceExpC) Coerce(v interface{}, path []string) (newv interface{}, err error) {
118 s, err := stringC.Coerce(v, path)
119 if err == nil {
120 newv = schema.MapType{
121 "interface": s,
122 "limit": c.limit,
123 "optional": false,
124 }
125 return
126 }
127
128 // Optional values are context-sensitive and/or have
129 // defaults, which is different than what KeyDict can
130 // readily support. So just do it here first, then
131 // coerce to the real schema.
132 v, err = mapC.Coerce(v, path)
133 if err != nil {
134 return
135 }
136 m := v.(schema.MapType)
137 if _, ok := m["limit"]; !ok {
138 m["limit"] = c.limit
139 }
140 if _, ok := m["optional"]; !ok {
141 m["optional"] = false
142 }
143 return ifaceSchema.Coerce(m, path)
144}
145
146var ifaceSchema = schema.FieldMap(schema.Fields{
147 "interface": schema.String(),
148 "limit": schema.OneOf(schema.Const(nil), schema.Int()),
149 "optional": schema.Bool(),
150}, nil)
151
152var charmSchema = schema.FieldMap(
153 schema.Fields{
154 "name": schema.String(),
155 "summary": schema.String(),
156 "description": schema.String(),
157 "peers": schema.Map(schema.String(), ifaceExpander(1)),
158 "provides": schema.Map(schema.String(), ifaceExpander(nil)),
159 "requires": schema.Map(schema.String(), ifaceExpander(1)),
160 "revision": schema.Int(), // Obsolete
161 },
162 schema.Optional{"provides", "requires", "peers", "revision"},
163)
0164
=== added file 'charm/meta_test.go'
--- charm/meta_test.go 1970-01-01 00:00:00 +0000
+++ charm/meta_test.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,108 @@
1package charm_test
2
3import (
4 "bytes"
5 "io"
6 "io/ioutil"
7 . "launchpad.net/gocheck"
8 "launchpad.net/juju/go/charm"
9 "launchpad.net/juju/go/schema"
10 "os"
11 "path/filepath"
12)
13
14func repoMeta(name string) io.Reader {
15 file, err := os.Open(filepath.Join("testrepo", "series", name, "metadata.yaml"))
16 if err != nil {
17 panic(err)
18 }
19 defer file.Close()
20 data, err := ioutil.ReadAll(file)
21 if err != nil {
22 panic(err)
23 }
24 return bytes.NewBuffer(data)
25}
26
27func (s *S) TestReadMeta(c *C) {
28 meta, err := charm.ReadMeta(repoMeta("dummy"))
29 c.Assert(err, IsNil)
30 c.Assert(meta.Name, Equals, "dummy")
31 c.Assert(meta.Summary, Equals, "That's a dummy charm.")
32 c.Assert(meta.Description, Equals,
33 "This is a longer description which\npotentially contains multiple lines.\n")
34 c.Assert(meta.OldRevision, Equals, 0)
35}
36
37func (s *S) TestParseMetaRelations(c *C) {
38 meta, err := charm.ReadMeta(repoMeta("mysql"))
39 c.Assert(err, IsNil)
40 c.Assert(meta.Provides["server"], Equals, charm.Relation{Interface: "mysql"})
41 c.Assert(meta.Requires, IsNil)
42 c.Assert(meta.Peers, IsNil)
43
44 meta, err = charm.ReadMeta(repoMeta("riak"))
45 c.Assert(err, IsNil)
46 c.Assert(meta.Provides["endpoint"], Equals, charm.Relation{Interface: "http"})
47 c.Assert(meta.Provides["admin"], Equals, charm.Relation{Interface: "http"})
48 c.Assert(meta.Peers["ring"], Equals, charm.Relation{Interface: "riak", Limit: 1})
49 c.Assert(meta.Requires, IsNil)
50
51 meta, err = charm.ReadMeta(repoMeta("wordpress"))
52 c.Assert(err, IsNil)
53 c.Assert(meta.Provides["url"], Equals, charm.Relation{Interface: "http"})
54 c.Assert(meta.Requires["db"], Equals, charm.Relation{Interface: "mysql", Limit: 1})
55 c.Assert(meta.Requires["cache"], Equals, charm.Relation{Interface: "varnish", Limit: 2, Optional: true})
56 c.Assert(meta.Peers, IsNil)
57
58}
59
60// Test rewriting of a given interface specification into long form.
61//
62// InterfaceExpander uses `coerce` to do one of two things:
63//
64// - Rewrite shorthand to the long form used for actual storage
65// - Fills in defaults, including a configurable `limit`
66//
67// This test ensures test coverage on each of these branches, along
68// with ensuring the conversion object properly raises SchemaError
69// exceptions on invalid data.
70func (s *S) TestIfaceExpander(c *C) {
71 e := charm.IfaceExpander(nil)
72
73 path := []string{"<pa", "th>"}
74
75 // Shorthand is properly rewritten
76 v, err := e.Coerce("http", path)
77 c.Assert(err, IsNil)
78 c.Assert(v, Equals, schema.MapType{"interface": "http", "limit": nil, "optional": false})
79
80 // Defaults are properly applied
81 v, err = e.Coerce(schema.MapType{"interface": "http"}, path)
82 c.Assert(err, IsNil)
83 c.Assert(v, Equals, schema.MapType{"interface": "http", "limit": nil, "optional": false})
84
85 v, err = e.Coerce(schema.MapType{"interface": "http", "limit": 2}, path)
86 c.Assert(err, IsNil)
87 c.Assert(v, Equals, schema.MapType{"interface": "http", "limit": int64(2), "optional": false})
88
89 v, err = e.Coerce(schema.MapType{"interface": "http", "optional": true}, path)
90 c.Assert(err, IsNil)
91 c.Assert(v, Equals, schema.MapType{"interface": "http", "limit": nil, "optional": true})
92
93 // Invalid data raises an error.
94 v, err = e.Coerce(42, path)
95 c.Assert(err, ErrorMatches, "<path>: expected map, got 42")
96
97 v, err = e.Coerce(schema.MapType{"interface": "http", "optional": nil}, path)
98 c.Assert(err, ErrorMatches, "<path>.optional: expected bool, got nothing")
99
100 v, err = e.Coerce(schema.MapType{"interface": "http", "limit": "none, really"}, path)
101 c.Assert(err, ErrorMatches, "<path>.limit: unsupported value")
102
103 // Can change default limit
104 e = charm.IfaceExpander(1)
105 v, err = e.Coerce(schema.MapType{"interface": "http"}, path)
106 c.Assert(err, IsNil)
107 c.Assert(v, Equals, schema.MapType{"interface": "http", "limit": int64(1), "optional": false})
108}
0109
=== added directory 'charm/testrepo'
=== added directory 'charm/testrepo/series'
=== added directory 'charm/testrepo/series/dummy'
=== added directory 'charm/testrepo/series/dummy/.dir'
=== added file 'charm/testrepo/series/dummy/.dir/ignored'
=== added file 'charm/testrepo/series/dummy/.ignored'
--- charm/testrepo/series/dummy/.ignored 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/dummy/.ignored 2012-01-18 17:30:30 +0000
@@ -0,0 +1,1 @@
1#
0\ No newline at end of file2\ No newline at end of file
13
=== added directory 'charm/testrepo/series/dummy/build'
=== added file 'charm/testrepo/series/dummy/build/ignored'
=== added file 'charm/testrepo/series/dummy/config.yaml'
--- charm/testrepo/series/dummy/config.yaml 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/dummy/config.yaml 2012-01-18 17:30:30 +0000
@@ -0,0 +1,5 @@
1options:
2 title: {default: My Title, description: A descriptive title used for the service., type: string}
3 outlook: {description: No default outlook., type: string}
4 username: {default: admin001, description: The name of the initial account (given admin permissions)., type: string}
5 skill-level: {description: A number indicating skill., type: int}
06
=== added directory 'charm/testrepo/series/dummy/empty'
=== added directory 'charm/testrepo/series/dummy/hooks'
=== added file 'charm/testrepo/series/dummy/hooks/install'
--- charm/testrepo/series/dummy/hooks/install 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/dummy/hooks/install 2012-01-18 17:30:30 +0000
@@ -0,0 +1,2 @@
1#!/bin/bash
2echo "Done!"
03
=== added file 'charm/testrepo/series/dummy/metadata.yaml'
--- charm/testrepo/series/dummy/metadata.yaml 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/dummy/metadata.yaml 2012-01-18 17:30:30 +0000
@@ -0,0 +1,5 @@
1name: dummy
2summary: "That's a dummy charm."
3description: |
4 This is a longer description which
5 potentially contains multiple lines.
06
=== added file 'charm/testrepo/series/dummy/revision'
--- charm/testrepo/series/dummy/revision 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/dummy/revision 2012-01-18 17:30:30 +0000
@@ -0,0 +1,1 @@
11
0\ No newline at end of file2\ No newline at end of file
13
=== added directory 'charm/testrepo/series/dummy/src'
=== added file 'charm/testrepo/series/dummy/src/hello.c'
--- charm/testrepo/series/dummy/src/hello.c 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/dummy/src/hello.c 2012-01-18 17:30:30 +0000
@@ -0,0 +1,7 @@
1#include <stdio.h>
2
3main()
4{
5 printf ("Hello World!\n");
6 return 0;
7}
08
=== added directory 'charm/testrepo/series/mysql'
=== added directory 'charm/testrepo/series/mysql-alternative'
=== added file 'charm/testrepo/series/mysql-alternative/metadata.yaml'
--- charm/testrepo/series/mysql-alternative/metadata.yaml 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/mysql-alternative/metadata.yaml 2012-01-18 17:30:30 +0000
@@ -0,0 +1,9 @@
1name: mysql-alternative
2summary: "Database engine"
3description: "A pretty popular database"
4provides:
5 prod:
6 interface: mysql
7 dev:
8 interface: mysql
9 limit: 2
010
=== added file 'charm/testrepo/series/mysql-alternative/revision'
--- charm/testrepo/series/mysql-alternative/revision 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/mysql-alternative/revision 2012-01-18 17:30:30 +0000
@@ -0,0 +1,1 @@
11
0\ No newline at end of file2\ No newline at end of file
13
=== added file 'charm/testrepo/series/mysql/metadata.yaml'
--- charm/testrepo/series/mysql/metadata.yaml 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/mysql/metadata.yaml 2012-01-18 17:30:30 +0000
@@ -0,0 +1,5 @@
1name: mysql
2summary: "Database engine"
3description: "A pretty popular database"
4provides:
5 server: mysql
06
=== added file 'charm/testrepo/series/mysql/revision'
--- charm/testrepo/series/mysql/revision 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/mysql/revision 2012-01-18 17:30:30 +0000
@@ -0,0 +1,1 @@
11
0\ No newline at end of file2\ No newline at end of file
13
=== added directory 'charm/testrepo/series/new'
=== added file 'charm/testrepo/series/new/metadata.yaml'
--- charm/testrepo/series/new/metadata.yaml 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/new/metadata.yaml 2012-01-18 17:30:30 +0000
@@ -0,0 +1,5 @@
1name: sample
2summary: "That's a sample charm."
3description: |
4 This is a longer description which
5 potentially contains multiple lines.
06
=== added file 'charm/testrepo/series/new/revision'
--- charm/testrepo/series/new/revision 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/new/revision 2012-01-18 17:30:30 +0000
@@ -0,0 +1,1 @@
12
02
=== added directory 'charm/testrepo/series/old'
=== added file 'charm/testrepo/series/old/metadata.yaml'
--- charm/testrepo/series/old/metadata.yaml 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/old/metadata.yaml 2012-01-18 17:30:30 +0000
@@ -0,0 +1,5 @@
1name: sample
2summary: "That's a sample charm."
3description: |
4 This is a longer description which
5 potentially contains multiple lines.
06
=== added file 'charm/testrepo/series/old/revision'
--- charm/testrepo/series/old/revision 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/old/revision 2012-01-18 17:30:30 +0000
@@ -0,0 +1,1 @@
11
02
=== added directory 'charm/testrepo/series/riak'
=== added file 'charm/testrepo/series/riak/metadata.yaml'
--- charm/testrepo/series/riak/metadata.yaml 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/riak/metadata.yaml 2012-01-18 17:30:30 +0000
@@ -0,0 +1,11 @@
1name: riak
2summary: "K/V storage engine"
3description: "Scalable K/V Store in Erlang with Clocks :-)"
4provides:
5 endpoint:
6 interface: http
7 admin:
8 interface: http
9peers:
10 ring:
11 interface: riak
012
=== added file 'charm/testrepo/series/riak/revision'
--- charm/testrepo/series/riak/revision 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/riak/revision 2012-01-18 17:30:30 +0000
@@ -0,0 +1,1 @@
17
0\ No newline at end of file2\ No newline at end of file
13
=== added directory 'charm/testrepo/series/varnish'
=== added directory 'charm/testrepo/series/varnish-alternative'
=== added directory 'charm/testrepo/series/varnish-alternative/hooks'
=== added file 'charm/testrepo/series/varnish-alternative/hooks/install'
--- charm/testrepo/series/varnish-alternative/hooks/install 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/varnish-alternative/hooks/install 2012-01-18 17:30:30 +0000
@@ -0,0 +1,3 @@
1#!/bin/bash
2
3echo hello world
0\ No newline at end of file4\ No newline at end of file
15
=== added file 'charm/testrepo/series/varnish-alternative/metadata.yaml'
--- charm/testrepo/series/varnish-alternative/metadata.yaml 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/varnish-alternative/metadata.yaml 2012-01-18 17:30:30 +0000
@@ -0,0 +1,5 @@
1name: varnish-alternative
2summary: "Database engine"
3description: "Another popular database"
4provides:
5 webcache: varnish
06
=== added file 'charm/testrepo/series/varnish-alternative/revision'
--- charm/testrepo/series/varnish-alternative/revision 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/varnish-alternative/revision 2012-01-18 17:30:30 +0000
@@ -0,0 +1,1 @@
11
0\ No newline at end of file2\ No newline at end of file
13
=== added file 'charm/testrepo/series/varnish/metadata.yaml'
--- charm/testrepo/series/varnish/metadata.yaml 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/varnish/metadata.yaml 2012-01-18 17:30:30 +0000
@@ -0,0 +1,5 @@
1name: varnish
2summary: "Database engine"
3description: "Another popular database"
4provides:
5 webcache: varnish
06
=== added file 'charm/testrepo/series/varnish/revision'
--- charm/testrepo/series/varnish/revision 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/varnish/revision 2012-01-18 17:30:30 +0000
@@ -0,0 +1,1 @@
11
0\ No newline at end of file2\ No newline at end of file
13
=== added directory 'charm/testrepo/series/wordpress'
=== added file 'charm/testrepo/series/wordpress/config.yaml'
--- charm/testrepo/series/wordpress/config.yaml 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/wordpress/config.yaml 2012-01-18 17:30:30 +0000
@@ -0,0 +1,3 @@
1options:
2 blog-title: {default: My Title, description: A descriptive title used for the blog., type: string}
3
04
=== added file 'charm/testrepo/series/wordpress/metadata.yaml'
--- charm/testrepo/series/wordpress/metadata.yaml 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/wordpress/metadata.yaml 2012-01-18 17:30:30 +0000
@@ -0,0 +1,19 @@
1name: wordpress
2summary: "Blog engine"
3description: "A pretty popular blog engine"
4provides:
5 url:
6 interface: http
7 limit:
8 optional: false
9requires:
10 db:
11 interface: mysql
12 limit: 1
13 optional: false
14 cache:
15 interface: varnish
16 limit: 2
17 optional: true
18
19
020
=== added file 'charm/testrepo/series/wordpress/revision'
--- charm/testrepo/series/wordpress/revision 1970-01-01 00:00:00 +0000
+++ charm/testrepo/series/wordpress/revision 2012-01-18 17:30:30 +0000
@@ -0,0 +1,1 @@
13
0\ No newline at end of file2\ No newline at end of file
13
=== added file 'charm/url.go'
--- charm/url.go 1970-01-01 00:00:00 +0000
+++ charm/url.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,132 @@
1package charm
2
3import (
4 "fmt"
5 "regexp"
6 "strconv"
7 "strings"
8)
9
10// A charm URL represents charm locations such as:
11//
12// cs:~joe/oneiric/wordpress
13// cs:oneiric/wordpress-42
14// local:oneiric/wordpress
15//
16type URL struct {
17 Name string
18 Revision int // -1 if unset, 0 is valid
19 Collection
20}
21
22// A charm Collection represents a namespace of charms. The
23// collection precedes the charm name in a charm URL.
24type Collection struct {
25 Schema string
26 User string
27 Series string
28}
29
30// WithRevision returns a *URL with the same Name and Collection of url,
31// but with Revision set to the revision parameter. If url already has
32// the requested revision, url itself is returned.
33func (url *URL) WithRevision(revision int) *URL {
34 if url.Revision == revision {
35 return url
36 }
37 urlCopy := *url
38 urlCopy.Revision = revision
39 return &urlCopy
40}
41
42var validUser = regexp.MustCompile("^[a-z0-9][a-zA-Z0-9+.-]+$")
43var validSeries = regexp.MustCompile("^[a-z]+([a-z-]+[a-z])?$")
44var validName = regexp.MustCompile("^[a-z][a-z0-9]*(-[a-z0-9]*[a-z][a-z0-9]*)*$")
45
46// MustParseURL works like ParseURL, but panics in case of errors.
47func MustParseURL(url string) *URL {
48 u, err := ParseURL(url)
49 if err != nil {
50 panic(err)
51 }
52 return u
53}
54
55// ParseURL parses the provided charm URL string into its respective
56// structure.
57func ParseURL(url string) (*URL, error) {
58 u := &URL{}
59 i := strings.Index(url, ":")
60 if i > 0 {
61 u.Schema = url[:i]
62 i++
63 }
64 // cs: or local:
65 if u.Schema != "cs" && u.Schema != "local" {
66 return nil, fmt.Errorf("charm URL has invalid schema: %q", url)
67 }
68 parts := strings.Split(url[i:], "/")
69 if len(parts) < 1 || len(parts) > 3 {
70 return nil, fmt.Errorf("charm URL has invalid form: %q", url)
71 }
72
73 // ~<username>
74 if strings.HasPrefix(parts[0], "~") {
75 if u.Schema == "local" {
76 return nil, fmt.Errorf("local charm URL with user name: %q", url)
77 }
78 u.User = parts[0][1:]
79 if !validUser.MatchString(u.User) {
80 return nil, fmt.Errorf("charm URL has invalid user name: %q", url)
81 }
82 parts = parts[1:]
83 }
84
85 // <series>
86 if len(parts) < 2 {
87 return nil, fmt.Errorf("charm URL without series: %q", url)
88 }
89 if len(parts) == 2 {
90 u.Series = parts[0]
91 if !validSeries.MatchString(u.Series) {
92 return nil, fmt.Errorf("charm URL has invalid series: %q", url)
93 }
94 parts = parts[1:]
95 }
96
97 // <name>[-<revision>]
98 u.Name = parts[0]
99 u.Revision = -1
100 for i := len(u.Name) - 1; i > 0; i-- {
101 c := u.Name[i]
102 if c >= '0' && c <= '9' {
103 continue
104 }
105 if c == '-' && i != len(u.Name)-1 {
106 var err error
107 u.Revision, err = strconv.Atoi(u.Name[i+1:])
108 if err != nil {
109 panic(err) // We just checked it was right.
110 }
111 u.Name = u.Name[:i]
112 }
113 break
114 }
115 if !validName.MatchString(u.Name) {
116 return nil, fmt.Errorf("charm URL has invalid charm name: %q", url)
117 }
118 return u, nil
119}
120
121func (u *URL) String() string {
122 if u.User != "" {
123 if u.Revision >= 0 {
124 return fmt.Sprintf("%s:~%s/%s/%s-%d", u.Schema, u.User, u.Series, u.Name, u.Revision)
125 }
126 return fmt.Sprintf("%s:~%s/%s/%s", u.Schema, u.User, u.Series, u.Name)
127 }
128 if u.Revision >= 0 {
129 return fmt.Sprintf("%s:%s/%s-%d", u.Schema, u.Series, u.Name, u.Revision)
130 }
131 return fmt.Sprintf("%s:%s/%s", u.Schema, u.Series, u.Name)
132}
0133
=== added file 'charm/url_test.go'
--- charm/url_test.go 1970-01-01 00:00:00 +0000
+++ charm/url_test.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,61 @@
1package charm_test
2
3import (
4 . "launchpad.net/gocheck"
5 "launchpad.net/juju/go/charm"
6)
7
8var urlTests = []struct {
9 s, err string
10 url *charm.URL
11}{
12 {"cs:~user/series/name", "", &charm.URL{"name", -1, charm.Collection{"cs", "user", "series"}}},
13 {"cs:~user/series/name-0", "", &charm.URL{"name", 0, charm.Collection{"cs", "user", "series"}}},
14 {"cs:series/name", "", &charm.URL{"name", -1, charm.Collection{"cs", "", "series"}}},
15 {"cs:series/name-42", "", &charm.URL{"name", 42, charm.Collection{"cs", "", "series"}}},
16 {"local:series/name-1", "", &charm.URL{"name", 1, charm.Collection{"local", "", "series"}}},
17 {"local:series/name", "", &charm.URL{"name", -1, charm.Collection{"local", "", "series"}}},
18 {"local:series/n0-0n-n0", "", &charm.URL{"n0-0n-n0", -1, charm.Collection{"local", "", "series"}}},
19
20 {"bs:~user/series/name-1", "charm URL has invalid schema: .*", nil},
21 {"cs:~1/series/name-1", "charm URL has invalid user name: .*", nil},
22 {"cs:~user/1/name-1", "charm URL has invalid series: .*", nil},
23 {"cs:~user/series/name-1-2", "charm URL has invalid charm name: .*", nil},
24 {"cs:~user/series/name-1-name-2", "charm URL has invalid charm name: .*", nil},
25 {"cs:~user/series/name--name-2", "charm URL has invalid charm name: .*", nil},
26 {"cs:~user/series/huh/name-1", "charm URL has invalid form: .*", nil},
27 {"cs:~user/name", "charm URL without series: .*", nil},
28 {"cs:name", "charm URL without series: .*", nil},
29 {"local:~user/series/name", "local charm URL with user name: .*", nil},
30 {"local:~user/name", "local charm URL with user name: .*", nil},
31 {"local:name", "charm URL without series: .*", nil},
32}
33
34func (s *S) TestParseURL(c *C) {
35 for _, t := range urlTests {
36 url, err := charm.ParseURL(t.s)
37 bug := Bug("ParseURL(%q)", t.s)
38 if t.err != "" {
39 c.Check(err.Error(), Matches, t.err, bug)
40 } else {
41 c.Check(url, Equals, t.url, bug)
42 c.Check(t.url.String(), Equals, t.s)
43 }
44 }
45}
46
47func (s *S) TestMustParseURL(c *C) {
48 url := charm.MustParseURL("cs:series/name")
49 c.Assert(url, Equals, &charm.URL{"name", -1, charm.Collection{"cs", "", "series"}})
50 f := func() { charm.MustParseURL("local:name") }
51 c.Assert(f, PanicMatches, "charm URL without series: .*")
52}
53
54func (s *S) TestWithRevision(c *C) {
55 url := charm.MustParseURL("cs:series/name")
56 other := url.WithRevision(1)
57 c.Assert(url, Equals, &charm.URL{"name", -1, charm.Collection{"cs", "", "series"}})
58 c.Assert(other, Equals, &charm.URL{"name", 1, charm.Collection{"cs", "", "series"}})
59
60 c.Assert(other.WithRevision(1) == other, Equals, true)
61}
062
=== added directory 'cloudinit'
=== added file 'cloudinit/Makefile'
--- cloudinit/Makefile 1970-01-01 00:00:00 +0000
+++ cloudinit/Makefile 2012-01-18 17:30:30 +0000
@@ -0,0 +1,24 @@
1include $(GOROOT)/src/Make.inc
2
3all: package
4
5TARG=launchpad.net/juju/go/cloudinit
6
7GOFILES=\
8 cloudinit.go\
9 options.go\
10
11GOFMT=gofmt
12BADFMT:=$(shell $(GOFMT) -l $(GOFILES) $(CGOFILES) $(wildcard *_test.go))
13
14gofmt: $(BADFMT)
15 @for F in $(BADFMT); do $(GOFMT) -w $$F && echo $$F; done
16
17ifneq ($(BADFMT),)
18ifneq ($(MAKECMDGOALS),gofmt)
19$(warning WARNING: make gofmt: $(BADFMT))
20endif
21endif
22
23include $(GOROOT)/src/Make.pkg
24
025
=== added file 'cloudinit/cloudinit.go'
--- cloudinit/cloudinit.go 1970-01-01 00:00:00 +0000
+++ cloudinit/cloudinit.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,66 @@
1// The cloudinit package implements a way of creating
2// a cloud-init configuration file.
3// See https://help.ubuntu.com/community/CloudInit.
4package cloudinit
5
6import (
7 yaml "launchpad.net/goyaml"
8)
9
10// Config represents a set of cloud-init configuration options.
11type Config struct {
12 attrs map[string]interface{}
13}
14
15// New returns a new Config with no options set.
16func New() *Config {
17 return &Config{make(map[string]interface{})}
18}
19
20// Render returns the cloud-init configuration as a YAML file.
21func (cfg *Config) Render() ([]byte, error) {
22 data, err := yaml.Marshal(cfg.attrs)
23 if err != nil {
24 return nil, err
25 }
26 return append([]byte("#cloud-config\n"), data...), nil
27}
28
29func (cfg *Config) set(opt string, yes bool, value interface{}) {
30 if yes {
31 cfg.attrs[opt] = value
32 } else {
33 delete(cfg.attrs, opt)
34 }
35}
36
37// source is Key, or KeyId and KeyServer
38type source struct {
39 Source string `yaml:"source"`
40 Key string `yaml:"key,omitempty"`
41 KeyId string `yaml:"keyid,omitempty"`
42 KeyServer string `yaml:"keyserver,omitempty"`
43}
44
45// command represents a shell command.
46type command struct {
47 literal string
48 args []string
49}
50
51// GetYAML implements yaml.Getter
52func (t *command) GetYAML() (tag string, value interface{}) {
53 if t.args != nil {
54 return "", t.args
55 }
56 return "", t.literal
57}
58
59type SSHKeyType string
60
61const (
62 RSAPrivate SSHKeyType = "rsa_private"
63 RSAPublic SSHKeyType = "rsa_public"
64 DSAPrivate SSHKeyType = "dsa_private"
65 DSAPublic SSHKeyType = "dsa_public"
66)
067
=== added file 'cloudinit/cloudinit_test.go'
--- cloudinit/cloudinit_test.go 1970-01-01 00:00:00 +0000
+++ cloudinit/cloudinit_test.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,200 @@
1package cloudinit_test
2
3import (
4 "fmt"
5 . "launchpad.net/gocheck"
6 "launchpad.net/juju/go/cloudinit"
7 "testing"
8)
9
10// TODO integration tests, but how?
11
12type S struct{}
13
14var _ = Suite(S{})
15
16func Test1(t *testing.T) {
17 TestingT(t)
18}
19
20var ctests = []struct {
21 name string
22 expect string
23 setOption func(cfg *cloudinit.Config)
24}{
25 {
26 "User",
27 "user: me\n",
28 func(cfg *cloudinit.Config) {
29 cfg.SetUser("me")
30 },
31 },
32 {
33 "AptUpgrade",
34 "apt_upgrade: true\n",
35 func(cfg *cloudinit.Config) {
36 cfg.SetAptUpgrade(true)
37 },
38 },
39 {
40 "AptUpdate",
41 "apt_update: true\n",
42 func(cfg *cloudinit.Config) {
43 cfg.SetAptUpdate(true)
44 },
45 },
46 {
47 "AptMirror",
48 "apt_mirror: http://foo.com\n",
49 func(cfg *cloudinit.Config) {
50 cfg.SetAptMirror("http://foo.com")
51 },
52 },
53 {
54 "AptPreserveSourcesList",
55 "apt_mirror: true\n",
56 func(cfg *cloudinit.Config) {
57 cfg.SetAptPreserveSourcesList(true)
58 },
59 },
60 {
61 "DebconfSelections",
62 "debconf_selections: '# Force debconf priority to critical.\n\n debconf debconf/priority select critical\n\n'\n",
63 func(cfg *cloudinit.Config) {
64 cfg.SetDebconfSelections("# Force debconf priority to critical.\ndebconf debconf/priority select critical\n")
65 },
66 },
67 {
68 "DisableEC2Metadata",
69 "disable_ec2_metadata: true\n",
70 func(cfg *cloudinit.Config) {
71 cfg.SetDisableEC2Metadata(true)
72 },
73 },
74 {
75 "FinalMessage",
76 "final_message: goodbye\n",
77 func(cfg *cloudinit.Config) {
78 cfg.SetFinalMessage("goodbye")
79 },
80 },
81 {
82 "Locale",
83 "locale: en_us\n",
84 func(cfg *cloudinit.Config) {
85 cfg.SetLocale("en_us")
86 },
87 },
88 {
89 "DisableRoot",
90 "disable_root: false\n",
91 func(cfg *cloudinit.Config) {
92 cfg.SetDisableRoot(false)
93 },
94 },
95 {
96 "SSHAuthorizedKeys",
97 "ssh_authorized_keys:\n- key1\n- key2\n",
98 func(cfg *cloudinit.Config) {
99 cfg.AddSSHAuthorizedKey("key1")
100 cfg.AddSSHAuthorizedKey("key2")
101 },
102 },
103 {
104 "SSHKeys RSA",
105 "ssh_keys:\n rsa_private: key1data\n rsa_public: key2data\n",
106 func(cfg *cloudinit.Config) {
107 cfg.AddSSHKey(cloudinit.RSAPrivate, "key1data")
108 cfg.AddSSHKey(cloudinit.RSAPublic, "key2data")
109 },
110 },
111 {
112 "SSHKeys DSA",
113 "ssh_keys:\n dsa_public: key1data\n dsa_private: key2data\n",
114 func(cfg *cloudinit.Config) {
115 cfg.AddSSHKey(cloudinit.DSAPublic, "key1data")
116 cfg.AddSSHKey(cloudinit.DSAPrivate, "key2data")
117 },
118 },
119 {
120 "Output",
121 "output:\n all:\n - '>foo'\n - '|bar'\n",
122 func(cfg *cloudinit.Config) {
123 cfg.SetOutput("all", ">foo", "|bar")
124 },
125 },
126 {
127 "Output",
128 "output:\n all: '>foo'\n",
129 func(cfg *cloudinit.Config) {
130 cfg.SetOutput(cloudinit.OutAll, ">foo", "")
131 },
132 },
133 {
134 "AptSources",
135 "apt_sources:\n- source: keyName\n key: someKey\n",
136 func(cfg *cloudinit.Config) {
137 cfg.AddAptSource("keyName", "someKey")
138 },
139 },
140 {
141 "AptSources",
142 "apt_sources:\n- source: keyName\n keyid: someKey\n keyserver: foo.com\n",
143 func(cfg *cloudinit.Config) {
144 cfg.AddAptSourceWithKeyId("keyName", "someKey", "foo.com")
145 },
146 },
147 {
148 "Packages",
149 "packages:\n- juju\n- ubuntu\n",
150 func(cfg *cloudinit.Config) {
151 cfg.AddPackage("juju")
152 cfg.AddPackage("ubuntu")
153 },
154 },
155 {
156 "BootCmd",
157 "bootcmd:\n- ls > /dev\n- - ls\n - '>with space'\n",
158 func(cfg *cloudinit.Config) {
159 cfg.AddBootCmd("ls > /dev")
160 cfg.AddBootCmdArgs("ls", ">with space")
161 },
162 },
163 {
164 "Mounts",
165 "mounts:\n- - x\n - \"y\"\n- - z\n - w\n",
166 func(cfg *cloudinit.Config) {
167 cfg.AddMount("x", "y")
168 cfg.AddMount("z", "w")
169 },
170 },
171}
172
173const header = "#cloud-config\n"
174
175func (S) TestOutput(c *C) {
176 for _, t := range ctests {
177 cfg := cloudinit.New()
178 t.setOption(cfg)
179 data, err := cfg.Render()
180 c.Assert(err, IsNil)
181 c.Assert(data, NotNil)
182 c.Assert(string(data), Equals, header+t.expect, Bug("test %q output differs", t.name))
183 }
184}
185
186//#cloud-config
187//packages:
188//- juju
189//- ubuntu
190func ExampleConfig() {
191 cfg := cloudinit.New()
192 cfg.AddPackage("juju")
193 cfg.AddPackage("ubuntu")
194 data, err := cfg.Render()
195 if err != nil {
196 fmt.Printf("render error: %v", err)
197 return
198 }
199 fmt.Printf("%s", data)
200}
0201
=== added file 'cloudinit/options.go'
--- cloudinit/options.go 1970-01-01 00:00:00 +0000
+++ cloudinit/options.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,214 @@
1package cloudinit
2
3// SetUser sets the user name that will be used for some other options.
4// The user will be assumed to already exist in the machine image.
5// The default user is "ubuntu".
6func (cfg *Config) SetUser(user string) {
7 cfg.set("user", user != "", user)
8}
9
10// SetAptUpgrade sets whether cloud-init runs "apt-get upgrade"
11// on first boot.
12func (cfg *Config) SetAptUpgrade(yes bool) {
13 cfg.set("apt_upgrade", yes, yes)
14}
15
16// SetUpdate sets whether cloud-init runs "apt-get update"
17// on first boot.
18func (cfg *Config) SetAptUpdate(yes bool) {
19 cfg.set("apt_update", yes, yes)
20}
21
22// SetAptMirror sets the URL to be used as the apt
23// mirror site. If not set, the URL is selected based
24// on cloud metadata in EC2 - <region>.archive.ubuntu.com
25func (cfg *Config) SetAptMirror(url string) {
26 cfg.set("apt_mirror", url != "", url)
27}
28
29// SetAptPreserveSourcesList sets whether /etc/apt/sources.list
30// is overwritten by the mirror. If true, SetAptMirror above
31// will have no effect.
32func (cfg *Config) SetAptPreserveSourcesList(yes bool) {
33 cfg.set("apt_mirror", yes, yes)
34}
35
36// AddAptSource adds an apt source. The key holds the
37// public key of the source, in the form expected by apt-key(8).
38func (cfg *Config) AddAptSource(name, key string) {
39 src, _ := cfg.attrs["apt_sources"].([]*source)
40 cfg.attrs["apt_sources"] = append(src,
41 &source{
42 Source: name,
43 Key: key,
44 })
45}
46
47// AddAptSource adds an apt source. The public key for the
48// source is retrieved by fetching the given keyId from the
49// GPG key server at the given address.
50func (cfg *Config) AddAptSourceWithKeyId(name, keyId, keyServer string) {
51 src, _ := cfg.attrs["apt_sources"].([]*source)
52 cfg.attrs["apt_sources"] = append(src,
53 &source{
54 Source: name,
55 KeyId: keyId,
56 KeyServer: keyServer,
57 })
58}
59
60// SetDebconfSelections provides preseeded debconf answers
61// for the boot process. The given answers will be used as input
62// to debconf-set-selections(1).
63func (cfg *Config) SetDebconfSelections(answers string) {
64 cfg.set("debconf_selections", answers != "", answers)
65}
66
67// AddPackage adds a package to be installed on first boot.
68// If any packages are specified, "apt-get update"
69// will be called.
70func (cfg *Config) AddPackage(name string) {
71 pkgs, _ := cfg.attrs["packages"].([]string)
72 cfg.attrs["packages"] = append(pkgs, name)
73}
74
75func (cfg *Config) addCmd(kind string, c *command) {
76 cmds, _ := cfg.attrs[kind].([]*command)
77 cfg.attrs[kind] = append(cmds, c)
78}
79
80// AddRunCmd adds a command to be executed
81// at first boot. The command will be run
82// by the shell with any metacharacters retaining
83// their special meaning (that is, no quoting takes place).
84func (cfg *Config) AddRunCmd(cmd string) {
85 cfg.addCmd("runcmd", &command{literal: cmd})
86}
87
88// AddRunCmdArgs is like AddRunCmd except that the command
89// will be executed with the given arguments properly quoted.
90func (cfg *Config) AddRunCmdArgs(args ...string) {
91 cfg.addCmd("runcmd", &command{args: args})
92}
93
94// AddBootCmd is like AddRunCmd except that the
95// command will run very early in the boot process,
96// and it will run on every boot, not just the first time.
97func (cfg *Config) AddBootCmd(cmd string) {
98 cfg.addCmd("bootcmd", &command{literal: cmd})
99}
100
101// AddBootCmdArgs is like AddBootCmd except that the command
102// will be executed with the given arguments properly quoted.
103func (cfg *Config) AddBootCmdArgs(args ...string) {
104 cfg.addCmd("bootcmd", &command{args: args})
105}
106
107// SetDisableEC2Metadata sets whether access to the
108// EC2 metadata service is disabled early in boot
109// via a null route ( route del -host 169.254.169.254 reject).
110func (cfg *Config) SetDisableEC2Metadata(yes bool) {
111 cfg.set("disable_ec2_metadata", yes, yes)
112}
113
114// SetFinalMessage sets to message that will be written
115// when the system has finished booting for the first time.
116// By default, the message is:
117// "cloud-init boot finished at $TIMESTAMP. Up $UPTIME seconds".
118func (cfg *Config) SetFinalMessage(msg string) {
119 cfg.set("final_message", msg != "", msg)
120}
121
122// SetLocale sets the locale; it defaults to en_US.UTF-8.
123func (cfg *Config) SetLocale(locale string) {
124 cfg.set("locale", locale != "", locale)
125}
126
127// AddMount adds a mount point. The given
128// arguments will be used as a line in /etc/fstab.
129func (cfg *Config) AddMount(args ...string) {
130 mounts, _ := cfg.attrs["mounts"].([][]string)
131 cfg.attrs["mounts"] = append(mounts, args)
132}
133
134// OutputKind represents a destination for command output.
135type OutputKind string
136
137const (
138 OutInit OutputKind = "init"
139 OutConfig OutputKind = "config"
140 OutFinal OutputKind = "final"
141 OutAll OutputKind = "all"
142)
143
144// SetOutput specifies destination for command output.
145// Valid values for the kind "init", "config", "final" and "all".
146// Each of stdout and stderr can take one of the following forms:
147// >>file
148// appends to file
149// >file
150// overwrites file
151// |command
152// pipes to the given command.
153func (cfg *Config) SetOutput(kind OutputKind, stdout, stderr string) {
154 out, _ := cfg.attrs["output"].(map[string]interface{})
155 if out == nil {
156 out = make(map[string]interface{})
157 }
158 if stderr == "" {
159 out[string(kind)] = stdout
160 } else {
161 out[string(kind)] = []string{stdout, stderr}
162 }
163 cfg.attrs["output"] = out
164}
165
166// AddSSHKey adds a pre-generated ssh key to the
167// server keyring. Keys that are added like this will be
168// written to /etc/ssh and new random keys will not
169// be generated.
170func (cfg *Config) AddSSHKey(keyType SSHKeyType, keyData string) {
171 keys, _ := cfg.attrs["ssh_keys"].(map[SSHKeyType]string)
172 if keys == nil {
173 keys = make(map[SSHKeyType]string)
174 cfg.attrs["ssh_keys"] = keys
175 }
176 keys[keyType] = keyData
177}
178
179// SetDisableRoot sets whether ssh login is disabled to the root account
180// via the ssh authorized key associated with the instance metadata.
181// It is true by default.
182func (cfg *Config) SetDisableRoot(disable bool) {
183 // note that disable_root defaults to true, so we include
184 // the option only if disable is false.
185 cfg.set("disable_root", !disable, disable)
186}
187
188// AddSSHAuthorizedKey adds a key that will be
189// an entry in ~/.ssh/authorized_keys for the
190// configured user (see SetUser).
191func (cfg *Config) AddSSHAuthorizedKey(yes string) {
192 keys, _ := cfg.attrs["ssh_authorized_keys"].([]string)
193 cfg.attrs["ssh_authorized_keys"] = append(keys, yes)
194}
195
196// TODO
197// byobu
198// grub_dpkg
199// mcollective
200// phone_home
201// puppet
202// resizefs
203// rightscale_userdata
204// rsyslog
205// scripts_per_boot
206// scripts_per_instance
207// scripts_per_once
208// scripts_user
209// set_hostname
210// set_passwords
211// ssh_import_id
212// timezone
213// update_etc_hosts
214// update_hostname
0215
=== added directory 'control'
=== added file 'control/bootstrap.go'
--- control/bootstrap.go 1970-01-01 00:00:00 +0000
+++ control/bootstrap.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,28 @@
1package control
2
3import "flag"
4import "fmt"
5
6type BootstrapCommand struct {
7 environment string
8}
9
10var _ Command = (*BootstrapCommand)(nil)
11
12func (c *BootstrapCommand) Parse(args []string) error {
13 fs := flag.NewFlagSet("bootstrap", flag.ExitOnError)
14 fs.StringVar(&c.environment, "e", "", "juju environment to operate in")
15 fs.StringVar(&c.environment, "environment", "", "juju environment to operate in")
16 if err := fs.Parse(args); err != nil {
17 return err
18 }
19 if len(fs.Args()) != 0 {
20 return fmt.Errorf("Unknown args: %s", fs.Args())
21 }
22 return nil
23}
24
25func (c *BootstrapCommand) Run() error {
26 fmt.Println("Running bootstrap in environment ", c.environment)
27 return nil
28}
029
=== added file 'control/command.go'
--- control/command.go 1970-01-01 00:00:00 +0000
+++ control/command.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,69 @@
1package control
2
3import "fmt"
4import "flag"
5
6type Command interface {
7 Parse(args []string) error
8 Run() error
9}
10
11type JujuCommand struct {
12 logfile string
13 verbose bool
14 subcmds map[string]Command
15 subcmd Command
16}
17
18func (c *JujuCommand) Logfile() string {
19 return c.logfile
20}
21
22func (c *JujuCommand) Verbose() bool {
23 return c.verbose
24}
25
26func (c *JujuCommand) Register(name string, subcmd Command) error {
27 if c.subcmds == nil {
28 c.subcmds = make(map[string]Command)
29 }
30 _, alreadythere := c.subcmds[name]
31 if alreadythere {
32 return fmt.Errorf("subcommand %s already registered", name)
33 }
34 c.subcmds[name] = subcmd
35 return nil
36}
37
38func (c *JujuCommand) Parse(args []string) error {
39 if len(args) == 0 {
40 return fmt.Errorf("no args to parse")
41 }
42 fs := flag.NewFlagSet(args[0], flag.ExitOnError)
43 fs.StringVar(&c.logfile, "l", "", "where to log to")
44 fs.StringVar(&c.logfile, "log-file", "", "where to log to")
45 fs.BoolVar(&c.verbose, "v", false, "whether to be noisy")
46 fs.BoolVar(&c.verbose, "verbose", false, "whether to be noisy")
47 if err := fs.Parse(args[1:]); err != nil {
48 return err
49 }
50 return c.parseSubcmd(fs.Args())
51}
52
53func (c *JujuCommand) parseSubcmd(args []string) error {
54 if len(args) == 0 {
55 return fmt.Errorf("no subcommand specified")
56 }
57 if c.subcmds == nil {
58 return fmt.Errorf("no subcommands registered")
59 }
60 exists := false
61 if c.subcmd, exists = c.subcmds[args[0]]; !exists {
62 return fmt.Errorf("no %s subcommand registered", args[0])
63 }
64 return c.subcmd.Parse(args[1:])
65}
66
67func (c *JujuCommand) Run() error {
68 return c.subcmd.Run()
69}
070
=== added file 'control/command_test.go'
--- control/command_test.go 1970-01-01 00:00:00 +0000
+++ control/command_test.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,100 @@
1package control_test
2
3import (
4 "flag"
5 . "launchpad.net/gocheck"
6 "launchpad.net/juju/go/control"
7 "testing"
8)
9
10func Test(t *testing.T) { TestingT(t) }
11
12type CommandSuite struct{}
13
14var _ = Suite(&CommandSuite{})
15
16type testCommand struct {
17 value string
18}
19
20func (c *testCommand) Parse(args []string) error {
21 fs := flag.NewFlagSet("defenestrate", flag.ContinueOnError)
22 fs.StringVar(&c.value, "value", "", "doc")
23 return fs.Parse(args)
24}
25
26func (c *testCommand) Run() error {
27 return nil
28}
29
30func parseEmpty(args []string) (*control.JujuCommand, error) {
31 jc := new(control.JujuCommand)
32 err := jc.Parse(args)
33 return jc, err
34}
35
36func parseDefenestrate(args []string) (*control.JujuCommand, *testCommand, error) {
37 jc := new(control.JujuCommand)
38 tc := new(testCommand)
39 jc.Register("defenestrate", tc)
40 err := jc.Parse(args)
41 return jc, tc, err
42}
43
44func (s *CommandSuite) TestSubcommandDispatch(c *C) {
45 _, err := parseEmpty([]string{"juju"})
46 c.Assert(err, ErrorMatches, `no subcommand specified`)
47
48 _, err = parseEmpty([]string{"juju", "defenstrate"})
49 c.Assert(err, ErrorMatches, `no subcommands registered`)
50
51 _, _, err = parseDefenestrate([]string{"juju", "discombobulate"})
52 c.Assert(err, ErrorMatches, `no discombobulate subcommand registered`)
53
54 _, tc, err := parseDefenestrate([]string{"juju", "defenestrate"})
55 c.Assert(err, IsNil)
56 c.Assert(tc.value, Equals, "")
57
58 _, tc, err = parseDefenestrate([]string{"juju", "defenestrate", "--value", "firmly"})
59 c.Assert(err, IsNil)
60 c.Assert(tc.value, Equals, "firmly")
61
62 _, tc, err = parseDefenestrate([]string{"juju", "defenestrate", "--gibberish", "burble"})
63 c.Assert(err, ErrorMatches, "flag provided but not defined: -gibberish")
64}
65
66func (s *CommandSuite) TestVerbose(c *C) {
67 jc, err := parseEmpty([]string{"juju"})
68 c.Assert(err, ErrorMatches, "no subcommand specified")
69 c.Assert(jc.Verbose(), Equals, false)
70
71 jc, _, err = parseDefenestrate([]string{"juju", "defenestrate"})
72 c.Assert(err, IsNil)
73 c.Assert(jc.Verbose(), Equals, false)
74
75 jc, err = parseEmpty([]string{"juju", "--verbose"})
76 c.Assert(err, ErrorMatches, "no subcommand specified")
77 c.Assert(jc.Verbose(), Equals, true)
78
79 jc, _, err = parseDefenestrate([]string{"juju", "-v", "defenestrate"})
80 c.Assert(err, IsNil)
81 c.Assert(jc.Verbose(), Equals, true)
82}
83
84func (s *CommandSuite) TestLogfile(c *C) {
85 jc, err := parseEmpty([]string{"juju"})
86 c.Assert(err, ErrorMatches, "no subcommand specified")
87 c.Assert(jc.Logfile(), Equals, "")
88
89 jc, _, err = parseDefenestrate([]string{"juju", "defenestrate"})
90 c.Assert(err, IsNil)
91 c.Assert(jc.Logfile(), Equals, "")
92
93 jc, err = parseEmpty([]string{"juju", "-l", "foo"})
94 c.Assert(err, ErrorMatches, "no subcommand specified")
95 c.Assert(jc.Logfile(), Equals, "foo")
96
97 jc, _, err = parseDefenestrate([]string{"juju", "--log-file", "bar", "defenestrate"})
98 c.Assert(err, IsNil)
99 c.Assert(jc.Logfile(), Equals, "bar")
100}
0101
=== added directory 'environs'
=== added file 'environs/Makefile'
--- environs/Makefile 1970-01-01 00:00:00 +0000
+++ environs/Makefile 2012-01-18 17:30:30 +0000
@@ -0,0 +1,25 @@
1include $(GOROOT)/src/Make.inc
2
3all: package
4
5TARG=launchpad.net/juju/go/environs
6
7GOFILES=\
8 open.go\
9 config.go\
10 interface.go\
11
12GOFMT=gofmt
13BADFMT:=$(shell $(GOFMT) -l $(GOFILES) $(CGOFILES) $(wildcard *_test.go))
14
15gofmt: $(BADFMT)
16 @for F in $(BADFMT); do $(GOFMT) -w $$F && echo $$F; done
17
18ifneq ($(BADFMT),)
19ifneq ($(MAKECMDGOALS),gofmt)
20$(warning WARNING: make gofmt: $(BADFMT))
21endif
22endif
23
24include $(GOROOT)/src/Make.pkg
25
026
=== added file 'environs/config.go'
--- environs/config.go 1970-01-01 00:00:00 +0000
+++ environs/config.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,134 @@
1package environs
2
3import (
4 "errors"
5 "fmt"
6 "io/ioutil"
7 "launchpad.net/goyaml"
8 "os"
9 "path/filepath"
10)
11
12// environ holds information about one environment.
13type environ struct {
14 kind string // the type of environment (e.g. ec2).
15 config interface{} // the configuration data for passing to NewEnviron.
16 err error // an error if the config data could not be parsed.
17}
18
19// Environs holds information about each named environment
20// in an environments.yaml file.
21type Environs struct {
22 Default string // The name of the default environment.
23 environs map[string]environ
24}
25
26// Names returns the list of environment names.
27func (e *Environs) Names() (names []string) {
28 for name := range e.environs {
29 names = append(names, name)
30 }
31 return
32}
33
34// providers maps from provider type to EnvironProvider for
35// each registered provider type.
36var providers = make(map[string]EnvironProvider)
37
38// RegisterProvider registers a new environment provider. Name gives the name
39// of the provider, and p the interface to that provider.
40//
41// RegisterProvider will panic if the same provider name is registered more than
42// once.
43func RegisterProvider(name string, p EnvironProvider) {
44 if providers[name] != nil {
45 panic(fmt.Errorf("juju: duplicate provider name %q", name))
46 }
47 providers[name] = p
48}
49
50// ReadEnvironsBytes parses the contents of an environments.yaml file
51// and returns its representation. An environment with an unknown type
52// will only generate an error when New is called for that environment.
53// Attributes for environments with known types are checked.
54func ReadEnvironsBytes(data []byte) (*Environs, error) {
55 var raw struct {
56 Default string "default"
57 Environments map[string]interface{} "environments"
58 }
59 raw.Environments = make(map[string]interface{}) // TODO fix bug in goyaml - it should make this automatically.
60 err := goyaml.Unmarshal(data, &raw)
61 if err != nil {
62 return nil, err
63 }
64
65 if raw.Default != "" && raw.Environments[raw.Default] == nil {
66 return nil, fmt.Errorf("default environment %q does not exist", raw.Default)
67 }
68 if raw.Default == "" {
69 // If there's a single environment, then we get the default
70 // automatically.
71 if len(raw.Environments) == 1 {
72 for name := range raw.Environments {
73 raw.Default = name
74 break
75 }
76 }
77 }
78
79 environs := make(map[string]environ)
80 for name, x := range raw.Environments {
81 attrs, ok := x.(map[interface{}]interface{})
82 if !ok {
83 return nil, fmt.Errorf("environment %q does not have attributes", name)
84 }
85 kind, _ := attrs["type"].(string)
86 if kind == "" {
87 return nil, fmt.Errorf("environment %q has no type", name)
88 }
89
90 p := providers[kind]
91 if p == nil {
92 // unknown provider type - skip entry but leave error message
93 // in case the environment is used later.
94 environs[name] = environ{
95 kind: kind,
96 err: fmt.Errorf("environment %q has an unknown provider type: %q", name, kind),
97 }
98 continue
99 }
100 cfg, err := p.ConfigChecker().Coerce(attrs, nil)
101 if err != nil {
102 return nil, fmt.Errorf("error parsing environment %q: %v", name, err)
103 }
104 environs[name] = environ{
105 kind: kind,
106 config: cfg,
107 }
108 }
109 return &Environs{raw.Default, environs}, nil
110}
111
112// ReadEnvirons reads the juju environments.yaml file
113// and returns the result of running ParseEnvironments
114// on the file's contents.
115// If environsFile is empty, $HOME/.juju/environments.yaml
116// is used.
117func ReadEnvirons(environsFile string) (*Environs, error) {
118 if environsFile == "" {
119 home := os.Getenv("HOME")
120 if home == "" {
121 return nil, errors.New("$HOME not set")
122 }
123 environsFile = filepath.Join(home, ".juju/environments.yaml")
124 }
125 data, err := ioutil.ReadFile(environsFile)
126 if err != nil {
127 return nil, err
128 }
129 e, err := ReadEnvironsBytes(data)
130 if err != nil {
131 return nil, fmt.Errorf("cannot parse %q: %v", environsFile, err)
132 }
133 return e, nil
134}
0135
=== added file 'environs/config_test.go'
--- environs/config_test.go 1970-01-01 00:00:00 +0000
+++ environs/config_test.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,179 @@
1package environs_test
2
3import (
4 "io/ioutil"
5 . "launchpad.net/gocheck"
6 "launchpad.net/juju/go/environs"
7 "os"
8 "path/filepath"
9)
10
11type configTest struct {
12 env string
13 check func(c *C, es *environs.Environs)
14}
15
16var configTests = []struct {
17 env string
18 check func(c *C, es *environs.Environs)
19}{
20 {`
21environments:
22 only:
23 type: unknown
24 other: anything
25`, func(c *C, es *environs.Environs) {
26 e, err := es.Open("")
27 c.Assert(e, IsNil)
28 c.Assert(err, NotNil)
29 c.Assert(err.Error(), Equals, `environment "only" has an unknown provider type: "unknown"`)
30 },
31 },
32 // one known environment, no defaults, bad attribute -> parse error
33 {`
34environments:
35 only:
36 type: dummy
37 badattr: anything
38`, nil,
39 },
40 // one known environment, no defaults -> parse ok, instantiate ok
41 {`
42environments:
43 only:
44 type: dummy
45 basename: foo
46`, func(c *C, es *environs.Environs) {
47 e, err := es.Open("")
48 c.Assert(err, IsNil)
49 checkDummyEnviron(c, e, "foo")
50 },
51 },
52 // several environments, no defaults -> parse ok, instantiate maybe error
53 {`
54environments:
55 one:
56 type: dummy
57 basename: foo
58 two:
59 type: dummy
60 basename: bar
61`, func(c *C, es *environs.Environs) {
62 e, err := es.Open("")
63 c.Assert(err, NotNil)
64 e, err = es.Open("one")
65 c.Assert(err, IsNil)
66 checkDummyEnviron(c, e, "foo")
67 },
68 },
69 // several environments, default -> parse ok, instantiate ok
70 {`
71default:
72 two
73environments:
74 one:
75 type: dummy
76 basename: foo
77 two:
78 type: dummy
79 basename: bar
80`, func(c *C, es *environs.Environs) {
81 e, err := es.Open("")
82 c.Assert(err, IsNil)
83 checkDummyEnviron(c, e, "bar")
84 },
85 },
86}
87
88func checkDummyEnviron(c *C, e environs.Environ, basename string) {
89 i0, err := e.StartInstance(0)
90 c.Assert(err, IsNil)
91 c.Assert(i0, NotNil)
92 c.Assert(i0.DNSName(), Equals, basename+"-0")
93
94 is, err := e.Instances()
95 c.Assert(err, IsNil)
96 c.Assert(len(is), Equals, 1)
97 c.Assert(is[0], Equals, i0)
98
99 i1, err := e.StartInstance(1)
100 c.Assert(err, IsNil)
101 c.Assert(i1, NotNil)
102 c.Assert(i1.DNSName(), Equals, basename+"-1")
103
104 is, err = e.Instances()
105 c.Assert(err, IsNil)
106 c.Assert(len(is), Equals, 2)
107 if is[0] == i1 {
108 is[0], is[1] = is[1], is[0]
109 }
110 c.Assert(is[0], Equals, i0)
111 c.Assert(is[1], Equals, i1)
112
113 err = e.StopInstances([]environs.Instance{i0})
114 c.Assert(err, IsNil)
115
116 is, err = e.Instances()
117 c.Assert(err, IsNil)
118 c.Assert(len(is), Equals, 1)
119 c.Assert(is[0], Equals, i1)
120
121 err = e.Destroy()
122 c.Assert(err, IsNil)
123}
124
125func (suite) TestConfig(c *C) {
126 for i, t := range configTests {
127 c.Logf("running test %v", i)
128 es, err := environs.ReadEnvironsBytes([]byte(t.env))
129 if es == nil {
130 c.Logf("parse failed\n")
131 if t.check != nil {
132 c.Errorf("test %d failed: %v", i, err)
133 }
134 } else {
135 if t.check == nil {
136 c.Errorf("test %d parsed ok but should have failed", i)
137 continue
138 }
139 c.Logf("checking...")
140 t.check(c, es)
141 }
142 }
143}
144
145func (suite) TestConfigFile(c *C) {
146 d := c.MkDir()
147 err := os.Mkdir(filepath.Join(d, ".juju"), 0777)
148 c.Assert(err, IsNil)
149
150 path := filepath.Join(d, ".juju", "environments.yaml")
151 env := `
152environments:
153 only:
154 type: dummy
155 basename: foo
156`
157 err = ioutil.WriteFile(path, []byte(env), 0666)
158 c.Assert(err, IsNil)
159
160 // test reading from a named file
161 es, err := environs.ReadEnvirons(path)
162 c.Assert(err, IsNil)
163 e, err := es.Open("")
164 c.Assert(err, IsNil)
165 checkDummyEnviron(c, e, "foo")
166
167 // test reading from the default environments.yaml file.
168 h := os.Getenv("HOME")
169 os.Setenv("HOME", d)
170
171 es, err = environs.ReadEnvirons("")
172 c.Assert(err, IsNil)
173 e, err = es.Open("")
174 c.Assert(err, IsNil)
175 checkDummyEnviron(c, e, "foo")
176
177 // reset $HOME just in case something else relies on it.
178 os.Setenv("HOME", h)
179}
0180
=== added file 'environs/dummyprovider_test.go'
--- environs/dummyprovider_test.go 1970-01-01 00:00:00 +0000
+++ environs/dummyprovider_test.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,93 @@
1// Dummy is a bare minimum provider that doesn't actually do anything.
2// The configuration requires a single value, "basename", which
3// is used as the base name of any machines that are "created".
4// It has no persistent state.
5//
6// Note that this file contains no tests as such - it is
7// just used by the testing code.
8package environs_test
9
10import (
11 "fmt"
12 "launchpad.net/juju/go/environs"
13 "launchpad.net/juju/go/schema"
14 "sync"
15)
16
17func init() {
18 environs.RegisterProvider("dummy", dummyProvider{})
19}
20
21type dummyInstance struct {
22 name string
23}
24
25func (m *dummyInstance) Id() string {
26 return fmt.Sprintf("dummy-%s", m.name)
27}
28
29func (m *dummyInstance) DNSName() string {
30 return m.name
31}
32
33type dummyProvider struct{}
34
35func (dummyProvider) ConfigChecker() schema.Checker {
36 return schema.FieldMap(
37 schema.Fields{
38 "type": schema.Const("dummy"),
39 "basename": schema.String(),
40 },
41 nil,
42 )
43}
44
45type dummyEnviron struct {
46 mu sync.Mutex
47 baseName string
48 n int // instance count
49
50 instances map[string]*dummyInstance
51}
52
53func (dummyProvider) Open(name string, attributes interface{}) (e environs.Environ, err error) {
54 cfg := attributes.(schema.MapType)
55 return &dummyEnviron{
56 baseName: cfg["basename"].(string),
57 instances: make(map[string]*dummyInstance),
58 }, nil
59}
60
61func (*dummyEnviron) Destroy() error {
62 return nil
63}
64
65func (e *dummyEnviron) StartInstance(id int) (environs.Instance, error) {
66 e.mu.Lock()
67 defer e.mu.Unlock()
68 i := &dummyInstance{
69 name: fmt.Sprintf("%s-%d", e.baseName, e.n),
70 }
71 e.instances[i.name] = i
72 e.n++
73 return i, nil
74}
75
76func (e *dummyEnviron) StopInstances(is []environs.Instance) error {
77 e.mu.Lock()
78 defer e.mu.Unlock()
79 for _, i := range is {
80 delete(e.instances, i.(*dummyInstance).name)
81 }
82 return nil
83}
84
85func (e *dummyEnviron) Instances() ([]environs.Instance, error) {
86 e.mu.Lock()
87 defer e.mu.Unlock()
88 var is []environs.Instance
89 for _, i := range e.instances {
90 is = append(is, i)
91 }
92 return is, nil
93}
094
=== added directory 'environs/ec2'
=== added file 'environs/ec2/Makefile'
--- environs/ec2/Makefile 1970-01-01 00:00:00 +0000
+++ environs/ec2/Makefile 2012-01-18 17:30:30 +0000
@@ -0,0 +1,31 @@
1include $(GOROOT)/src/Make.inc
2
3all: package
4
5TARG=launchpad.net/juju/go/environs/ec2
6
7GOFILES=\
8 config.go\
9 ec2.go\
10 image.go\
11 util.go\
12
13GOFMT=gofmt
14BADFMT:=$(shell $(GOFMT) -l $(GOFILES) $(CGOFILES) $(wildcard *_test.go))
15
16gofmt: $(BADFMT)
17 @for F in $(BADFMT); do $(GOFMT) -w $$F && echo $$F; done
18
19ifneq ($(BADFMT),)
20ifneq ($(MAKECMDGOALS),gofmt)
21$(warning WARNING: make gofmt: $(BADFMT))
22endif
23endif
24
25include $(GOROOT)/src/Make.pkg
26
27# regenerate doesn't remove images that have disappeared
28# but that's probably not a problem.
29regenerate:
30 gotest -regenerate-images
31 bzr add images
032
=== added file 'environs/ec2/config.go'
--- environs/ec2/config.go 1970-01-01 00:00:00 +0000
+++ environs/ec2/config.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,79 @@
1package ec2
2
3import (
4 "fmt"
5 "launchpad.net/goamz/aws"
6 "launchpad.net/juju/go/schema"
7)
8
9// providerConfig is a placeholder for any config information
10// that we will have in a configuration file.
11type providerConfig struct {
12 region string
13 auth aws.Auth
14}
15
16type checker struct{}
17
18func (checker) Coerce(v interface{}, path []string) (interface{}, error) {
19 return &providerConfig{}, nil
20}
21
22// TODO move these known strings into goamz/aws
23var Regions = map[string]aws.Region{
24 "ap-northeast-1": aws.APNortheast,
25 "ap-southeast-1": aws.APSoutheast,
26 "eu-west-1": aws.EUWest,
27 "us-east-1": aws.USEast,
28 "us-west-1": aws.USWest,
29}
30
31func (environProvider) ConfigChecker() schema.Checker {
32 return combineCheckers(
33 schema.FieldMap(
34 schema.Fields{
35 "access-key": schema.String(),
36 "secret-key": schema.String(),
37 "region": schema.String(),
38 }, []string{
39 "access-key",
40 "secret-key",
41 "region",
42 },
43 ),
44 checkerFunc(func(v interface{}, path []string) (newv interface{}, err error) {
45 m := v.(schema.MapType)
46 var c providerConfig
47
48 c.auth.AccessKey = maybeString(m["access-key"], "")
49 c.auth.SecretKey = maybeString(m["secret-key"], "")
50 if c.auth.AccessKey == "" || c.auth.SecretKey == "" {
51 if c.auth.AccessKey != "" {
52 return nil, fmt.Errorf("environment has access-key but no secret-key")
53 }
54 if c.auth.SecretKey != "" {
55 return nil, fmt.Errorf("environment has secret-key but no access-key")
56 }
57 var err error
58 c.auth, err = aws.EnvAuth()
59 if err != nil {
60 return nil, err
61 }
62 }
63
64 regionName := maybeString(m["region"], "us-east-1")
65 if _, ok := Regions[regionName]; !ok {
66 return nil, fmt.Errorf("invalid region name %q", regionName)
67 }
68 c.region = regionName
69 return &c, nil
70 }),
71 )
72}
73
74func maybeString(x interface{}, dflt string) string {
75 if x == nil {
76 return dflt
77 }
78 return x.(string)
79}
080
=== added file 'environs/ec2/config_test.go'
--- environs/ec2/config_test.go 1970-01-01 00:00:00 +0000
+++ environs/ec2/config_test.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,104 @@
1package ec2
2
3import (
4 "launchpad.net/goamz/aws"
5 . "launchpad.net/gocheck"
6 "launchpad.net/juju/go/environs"
7 "os"
8 "strings"
9)
10
11// Use local suite since this file lives in the ec2 package
12// for testing internals.
13type configSuite struct{}
14
15var _ = Suite(configSuite{})
16
17var configTestRegion = aws.Region{
18 EC2Endpoint: "testregion.nowhere:1234",
19}
20
21var testAuth = aws.Auth{"gopher", "long teeth"}
22
23type configTest struct {
24 env string
25 config *providerConfig
26 err string
27}
28
29var configTests = []configTest{
30 {"", &providerConfig{region: "us-east-1", auth: testAuth}, ""},
31 {"region: eu-west-1\n", &providerConfig{region: "eu-west-1", auth: testAuth}, ""},
32 {"region: unknown\n", nil, ".*invalid region name.*"},
33 {"region: configtest\n", &providerConfig{region: "configtest", auth: testAuth}, ""},
34 {"region: 666\n", nil, ".*expected string, got 666"},
35 {"access-key: 666\n", nil, ".*expected string, got 666"},
36 {"secret-key: 666\n", nil, ".*expected string, got 666"},
37 {"access-key: jujuer\nsecret-key: open sesame\n",
38 &providerConfig{
39 region: "us-east-1",
40 auth: aws.Auth{
41 AccessKey: "jujuer",
42 SecretKey: "open sesame",
43 },
44 },
45 "",
46 },
47 {"access-key: jujuer\n", nil, ".*environment has access-key but no secret-key"},
48 {"secret-key: badness\n", nil, ".*environment has secret-key but no access-key"},
49 // unknown fields are discarded
50 {"unknown-something: 666\n", &providerConfig{region: "us-east-1", auth: testAuth}, ""},
51}
52
53func indent(s string, with string) string {
54 var r string
55 lines := strings.Split(s, "\n")
56 for _, l := range lines {
57 r += with + l + "\n"
58 }
59 return r
60}
61
62func makeEnv(s string) []byte {
63 return []byte("environments:\n testenv:\n type: ec2\n" + indent(s, " "))
64}
65
66func (configSuite) TestConfig(c *C) {
67 Regions["configtest"] = configTestRegion
68 defer delete(Regions, "configtest")
69
70 defer os.Setenv("AWS_ACCESS_KEY_ID", os.Getenv("AWS_ACCESS_KEY_ID"))
71 defer os.Setenv("AWS_SECRET_ACCESS_KEY", os.Getenv("AWS_SECRET_ACCESS_KEY"))
72
73 os.Setenv("AWS_ACCESS_KEY_ID", "")
74 os.Setenv("AWS_SECRET_ACCESS_KEY", "")
75
76 // first try with no auth environment vars set
77 test := configTest{"", &providerConfig{region: "us-east-1", auth: testAuth}, ".*not found in environment"}
78 test.run(c)
79
80 // then set testAuthults
81 os.Setenv("AWS_ACCESS_KEY_ID", testAuth.AccessKey)
82 os.Setenv("AWS_SECRET_ACCESS_KEY", testAuth.SecretKey)
83
84 for _, t := range configTests {
85 t.run(c)
86 }
87}
88
89func (t configTest) run(c *C) {
90 envs, err := environs.ReadEnvironsBytes(makeEnv(t.env))
91 if err != nil {
92 if t.err != "" {
93 c.Check(err, ErrorMatches, t.err, Bug("environ %q", t.env))
94 } else {
95 c.Check(err, IsNil, Bug("environ %q", t.env))
96 }
97 return
98 }
99 e, err := envs.Open("testenv")
100 c.Assert(err, IsNil)
101 c.Assert(e, NotNil)
102 c.Assert(e, FitsTypeOf, (*environ)(nil), Bug("environ %q", t.env))
103 c.Check(e.(*environ).config, Equals, t.config, Bug("environ %q", t.env))
104}
0105
=== added file 'environs/ec2/ec2.go'
--- environs/ec2/ec2.go 1970-01-01 00:00:00 +0000
+++ environs/ec2/ec2.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,171 @@
1package ec2
2
3import (
4 "fmt"
5 "launchpad.net/goamz/ec2"
6 "launchpad.net/juju/go/environs"
7)
8
9func init() {
10 environs.RegisterProvider("ec2", environProvider{})
11}
12
13type environProvider struct{}
14
15var _ environs.EnvironProvider = environProvider{}
16
17type environ struct {
18 name string
19 config *providerConfig
20 ec2 *ec2.EC2
21}
22
23var _ environs.Environ = (*environ)(nil)
24
25type instance struct {
26 *ec2.Instance
27}
28
29var _ environs.Instance = (*instance)(nil)
30
31func (inst *instance) Id() string {
32 return inst.InstanceId
33}
34
35func (inst *instance) DNSName() string {
36 return inst.Instance.DNSName
37}
38
39func (environProvider) Open(name string, config interface{}) (e environs.Environ, err error) {
40 cfg := config.(*providerConfig)
41 if Regions[cfg.region].EC2Endpoint == "" {
42 return nil, fmt.Errorf("no ec2 endpoint found for region %q, opening %q", cfg.region, name)
43 }
44 return &environ{
45 name: name,
46 config: cfg,
47 ec2: ec2.New(cfg.auth, Regions[cfg.region]),
48 }, nil
49}
50
51func (e *environ) StartInstance(machineId int) (environs.Instance, error) {
52 image, err := FindImageSpec(DefaultImageConstraint)
53 if err != nil {
54 return nil, fmt.Errorf("cannot find image: %v", err)
55 }
56 groups, err := e.setUpGroups(machineId)
57 if err != nil {
58 return nil, fmt.Errorf("cannot set up groups: %v", err)
59 }
60 instances, err := e.ec2.RunInstances(&ec2.RunInstances{
61 ImageId: image.ImageId,
62 MinCount: 1,
63 MaxCount: 1,
64 UserData: nil,
65 InstanceType: "m1.small",
66 SecurityGroups: groups,
67 })
68 if err != nil {
69 return nil, fmt.Errorf("cannot run instances: %v", err)
70 }
71 if len(instances.Instances) != 1 {
72 return nil, fmt.Errorf("expected 1 started instance, got %d", len(instances.Instances))
73 }
74 return &instance{&instances.Instances[0]}, nil
75}
76
77func (e *environ) StopInstances(insts []environs.Instance) error {
78 if len(insts) == 0 {
79 return nil
80 }
81 names := make([]string, len(insts))
82 for i, inst := range insts {
83 names[i] = inst.(*instance).InstanceId
84 }
85 _, err := e.ec2.TerminateInstances(names)
86 return err
87}
88
89func (e *environ) Instances() ([]environs.Instance, error) {
90 filter := ec2.NewFilter()
91 filter.Add("instance-state-name", "pending", "running")
92
93 resp, err := e.ec2.Instances(nil, filter)
94 if err != nil {
95 return nil, err
96 }
97 var insts []environs.Instance
98 for i := range resp.Reservations {
99 r := &resp.Reservations[i]
100 for j := range r.Instances {
101 insts = append(insts, &instance{&r.Instances[j]})
102 }
103 }
104 return insts, nil
105}
106
107func (e *environ) Destroy() error {
108 insts, err := e.Instances()
109 if err != nil {
110 return err
111 }
112 return e.StopInstances(insts)
113}
114
115func (e *environ) machineGroupName(machineId int) string {
116 return fmt.Sprintf("%s-%d", e.groupName(), machineId)
117}
118
119func (e *environ) groupName() string {
120 return "juju-" + e.name
121}
122
123// setUpGroups creates the security groups for the new machine, and
124// returns them.
125//
126// Instances are tagged with a group so they can be distinguished from
127// other instances that might be running on the same EC2 account. In
128// addition, a specific machine security group is created for each
129// machine, so that its firewall rules can be configured per machine.
130func (e *environ) setUpGroups(machineId int) ([]ec2.SecurityGroup, error) {
131 jujuGroup := ec2.SecurityGroup{Name: e.groupName()}
132 jujuMachineGroup := ec2.SecurityGroup{Name: e.machineGroupName(machineId)}
133 groups, err := e.ec2.SecurityGroups([]ec2.SecurityGroup{jujuGroup, jujuMachineGroup}, nil)
134 if err != nil {
135 return nil, fmt.Errorf("cannot get security groups: %v", err)
136 }
137
138 for _, g := range groups.Groups {
139 switch g.Name {
140 case jujuGroup.Name:
141 jujuGroup = g.SecurityGroup
142 case jujuMachineGroup.Name:
143 jujuMachineGroup = g.SecurityGroup
144 }
145 }
146
147 // Create the provider group if doesn't exist.
148 if jujuGroup.Id == "" {
149 r, err := e.ec2.CreateSecurityGroup(jujuGroup.Name, "juju group for "+e.name)
150 if err != nil {
151 return nil, fmt.Errorf("cannot create juju security group: %v", err)
152 }
153 jujuGroup = r.SecurityGroup
154 }
155
156 // Create the machine-specific group, but first see if there's
157 // one already existing from a previous machine launch;
158 // if so, delete it, since it can have the wrong firewall setup
159 if jujuMachineGroup.Id != "" {
160 _, err := e.ec2.DeleteSecurityGroup(jujuMachineGroup)
161 if err != nil {
162 return nil, fmt.Errorf("cannot delete old security group %q: %v", jujuMachineGroup.Name, err)
163 }
164 }
165 descr := fmt.Sprintf("juju group for %s machine %d", e.name, machineId)
166 r, err := e.ec2.CreateSecurityGroup(jujuMachineGroup.Name, descr)
167 if err != nil {
168 return nil, fmt.Errorf("cannot create machine group %q: %v", jujuMachineGroup.Name, err)
169 }
170 return []ec2.SecurityGroup{jujuGroup, r.SecurityGroup}, nil
171}
0172
=== added file 'environs/ec2/image.go'
--- environs/ec2/image.go 1970-01-01 00:00:00 +0000
+++ environs/ec2/image.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,80 @@
1package ec2
2
3import (
4 "bufio"
5 "fmt"
6 "net/http"
7 "strings"
8)
9
10// ImageConstraint specifies a range of possible machine images.
11// TODO allow specification of softer constraints?
12type ImageConstraint struct {
13 UbuntuRelease string
14 Architecture string
15 PersistentStorage bool
16 Region string
17 Daily bool
18 Desktop bool
19}
20
21var DefaultImageConstraint = &ImageConstraint{
22 UbuntuRelease: "oneiric",
23 Architecture: "i386",
24 PersistentStorage: true,
25 Region: "us-east-1",
26 Daily: false,
27 Desktop: false,
28}
29
30type ImageSpec struct {
31 ImageId string
32}
33
34func FindImageSpec(spec *ImageConstraint) (*ImageSpec, error) {
35 // note: original get_image_id added three optional args:
36 // DefaultImageId if found, returns that immediately
37 // Region overrides spec.Region
38 // DefaultSeries used if spec.UbuntuRelease is ""
39
40 hclient := new(http.Client)
41 uri := fmt.Sprintf("http://uec-images.ubuntu.com/query/%s/%s/%s.current.txt",
42 spec.UbuntuRelease,
43 either(spec.Desktop, "desktop", "server"), // variant.
44 either(spec.Daily, "daily", "released"), // version.
45 )
46 resp, err := hclient.Get(uri)
47 if err == nil && resp.StatusCode != 200 {
48 err = fmt.Errorf("%s", resp.Status)
49 }
50 if err != nil {
51 return nil, fmt.Errorf("error getting instance types: %v", err)
52 }
53 defer resp.Body.Close()
54 ebsMatch := either(spec.PersistentStorage, "ebs", "instance-store")
55 r := bufio.NewReader(resp.Body)
56 for {
57 line, _, err := r.ReadLine()
58 if err != nil {
59 return nil, fmt.Errorf("cannot find matching image: %v", err)
60 }
61 f := strings.Split(string(line), "\t")
62 if len(f) < 8 {
63 continue
64 }
65 if f[4] != ebsMatch {
66 continue
67 }
68 if f[5] == spec.Architecture && f[6] == spec.Region {
69 return &ImageSpec{f[7]}, nil
70 }
71 }
72 panic("not reached")
73}
74
75func either(yes bool, a, b string) string {
76 if yes {
77 return a
78 }
79 return b
80}
081
=== added file 'environs/ec2/image_test.go'
--- environs/ec2/image_test.go 1970-01-01 00:00:00 +0000
+++ environs/ec2/image_test.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,138 @@
1package ec2_test
2
3import (
4 "fmt"
5 "io"
6 . "launchpad.net/gocheck"
7 "launchpad.net/juju/go/environs/ec2"
8 "net/http"
9 "os"
10 "path/filepath"
11 "testing"
12)
13
14// N.B. the image IDs in this test will need updating
15// if the image directory is regenerated.
16var imageTests = []struct {
17 constraint ec2.ImageConstraint
18 imageId string
19 err string
20}{
21 {*ec2.DefaultImageConstraint, "ami-a7f539ce", ""},
22 {ec2.ImageConstraint{
23 UbuntuRelease: "natty",
24 Architecture: "amd64",
25 PersistentStorage: false,
26 Region: "eu-west-1",
27 Daily: true,
28 Desktop: true,
29 }, "ami-19fdc16d", ""},
30 {ec2.ImageConstraint{
31 UbuntuRelease: "natty",
32 Architecture: "i386",
33 PersistentStorage: true,
34 Region: "ap-northeast-1",
35 Daily: true,
36 Desktop: true,
37 }, "ami-cc9621cd", ""},
38 {ec2.ImageConstraint{
39 UbuntuRelease: "natty",
40 Architecture: "i386",
41 PersistentStorage: false,
42 Region: "ap-northeast-1",
43 Daily: true,
44 Desktop: true,
45 }, "ami-62962163", ""},
46 {ec2.ImageConstraint{
47 UbuntuRelease: "natty",
48 Architecture: "amd64",
49 PersistentStorage: false,
50 Region: "ap-northeast-1",
51 Daily: true,
52 Desktop: true,
53 }, "ami-a69621a7", ""},
54 {ec2.ImageConstraint{
55 UbuntuRelease: "zingy",
56 Architecture: "amd64",
57 PersistentStorage: false,
58 Region: "eu-west-1",
59 Daily: true,
60 Desktop: true,
61 }, "", "error getting instance types:.*"},
62}
63
64func (suite) TestFindImageSpec(c *C) {
65 // set up http so that all requests will be satisfied from the images directory.
66 defer setTransport(setTransport(http.NewFileTransport(http.Dir("images"))))
67
68 for i, t := range imageTests {
69 id, err := ec2.FindImageSpec(&t.constraint)
70 if t.err != "" {
71 c.Check(err, ErrorMatches, t.err, Bug("test %d", i))
72 c.Check(id, IsNil, Bug("test %d", i))
73 continue
74 }
75 if !c.Check(err, IsNil, Bug("test %d", i)) {
76 continue
77 }
78 if !c.Check(id, NotNil, Bug("test %d", i)) {
79 continue
80 }
81 c.Check(id.ImageId, Equals, t.imageId)
82 }
83}
84
85func setTransport(t http.RoundTripper) (old http.RoundTripper) {
86 old = http.DefaultTransport
87 http.DefaultTransport = t
88 return
89}
90
91// regenerate all data inside the images directory.
92// N.B. this second-guesses the logic inside images.go
93func regenerateImages(t *testing.T) {
94 if err := os.RemoveAll(imagesRoot); err != nil {
95 t.Errorf("cannot remove old images: %v", err)
96 return
97 }
98 for _, variant := range []string{"desktop", "server"} {
99 for _, version := range []string{"daily", "released"} {
100 for _, release := range []string{"natty", "oneiric"} {
101 s := fmt.Sprintf("query/%s/%s/%s.current.txt", release, variant, version)
102 t.Logf("regenerating images from %q", s)
103 err := copylocal(s)
104 if err != nil {
105 t.Logf("regenerate: %v", err)
106 }
107 }
108 }
109 }
110}
111
112var imagesRoot = "images"
113
114func copylocal(s string) error {
115 r, err := http.Get("http://uec-images.ubuntu.com/" + s)
116 if err != nil {
117 return fmt.Errorf("get %q: %v", s, err)
118 }
119 defer r.Body.Close()
120 if r.StatusCode != 200 {
121 return fmt.Errorf("status on %q: %s", s, r.Status)
122 }
123 path := filepath.Join(filepath.FromSlash(imagesRoot), filepath.FromSlash(s))
124 d, _ := filepath.Split(path)
125 if err := os.MkdirAll(d, 0777); err != nil {
126 return err
127 }
128 file, err := os.Create(path)
129 if err != nil {
130 return err
131 }
132 defer file.Close()
133 _, err = io.Copy(file, r.Body)
134 if err != nil {
135 return fmt.Errorf("error copying image file: %v", err)
136 }
137 return nil
138}
0139
=== added directory 'environs/ec2/images'
=== added directory 'environs/ec2/images/query'
=== added directory 'environs/ec2/images/query/natty'
=== added directory 'environs/ec2/images/query/natty/desktop'
=== added file 'environs/ec2/images/query/natty/desktop/daily.current.txt'
--- environs/ec2/images/query/natty/desktop/daily.current.txt 1970-01-01 00:00:00 +0000
+++ environs/ec2/images/query/natty/desktop/daily.current.txt 2012-01-18 17:30:30 +0000
@@ -0,0 +1,25 @@
1natty desktop daily 20111205 ebs amd64 ap-northeast-1 ami-d89621d9 aki-d409a2d5 paravirtual
2natty desktop daily 20111205 ebs i386 ap-northeast-1 ami-cc9621cd aki-d209a2d3 paravirtual
3natty desktop daily 20111205 instance-store amd64 ap-northeast-1 ami-a69621a7 aki-d409a2d5 paravirtual
4natty desktop daily 20111205 instance-store i386 ap-northeast-1 ami-62962163 aki-d209a2d3 paravirtual
5natty desktop daily 20111205 ebs amd64 ap-southeast-1 ami-ac3471fe aki-11d5aa43 paravirtual
6natty desktop daily 20111205 ebs i386 ap-southeast-1 ami-a63471f4 aki-13d5aa41 paravirtual
7natty desktop daily 20111205 instance-store amd64 ap-southeast-1 ami-8a3471d8 aki-11d5aa43 paravirtual
8natty desktop daily 20111205 instance-store i386 ap-southeast-1 ami-ea3471b8 aki-13d5aa41 paravirtual
9natty desktop daily 20111205 ebs amd64 eu-west-1 ami-abfdc1df aki-4feec43b paravirtual
10natty desktop daily 20111205 ebs i386 eu-west-1 ami-c7fdc1b3 aki-4deec439 paravirtual
11natty desktop daily 20111205 instance-store amd64 eu-west-1 ami-19fdc16d aki-4feec43b paravirtual
12natty desktop daily 20111205 instance-store i386 eu-west-1 ami-79fdc10d aki-4deec439 paravirtual
13natty desktop daily 20111205 ebs amd64 us-east-1 ami-e7408b8e hvm
14natty desktop daily 20111205 ebs amd64 us-east-1 ami-37408b5e aki-427d952b paravirtual
15natty desktop daily 20111205 ebs i386 us-east-1 ami-5d408b34 aki-407d9529 paravirtual
16natty desktop daily 20111205 instance-store amd64 us-east-1 ami-e1418a88 aki-427d952b paravirtual
17natty desktop daily 20111205 instance-store i386 us-east-1 ami-914289f8 aki-407d9529 paravirtual
18natty desktop daily 20111205 ebs amd64 us-west-1 ami-23a9f666 aki-9ba0f1de paravirtual
19natty desktop daily 20111205 ebs i386 us-west-1 ami-1ba9f65e aki-99a0f1dc paravirtual
20natty desktop daily 20111205 instance-store amd64 us-west-1 ami-03a9f646 aki-9ba0f1de paravirtual
21natty desktop daily 20111205 instance-store i386 us-west-1 ami-77a9f632 aki-99a0f1dc paravirtual
22natty desktop daily 20111205 ebs amd64 us-west-2 ami-aa98159a aki-ace26f9c paravirtual
23natty desktop daily 20111205 ebs i386 us-west-2 ami-a2981592 aki-dce26fec paravirtual
24natty desktop daily 20111205 instance-store amd64 us-west-2 ami-bc98158c aki-ace26f9c paravirtual
25natty desktop daily 20111205 instance-store i386 us-west-2 ami-b0981580 aki-dce26fec paravirtual
026
=== added directory 'environs/ec2/images/query/natty/server'
=== added file 'environs/ec2/images/query/natty/server/daily.current.txt'
--- environs/ec2/images/query/natty/server/daily.current.txt 1970-01-01 00:00:00 +0000
+++ environs/ec2/images/query/natty/server/daily.current.txt 2012-01-18 17:30:30 +0000
@@ -0,0 +1,25 @@
1natty server daily 20111201 ebs amd64 ap-northeast-1 ami-a6b80fa7 aki-d409a2d5 paravirtual
2natty server daily 20111201 ebs i386 ap-northeast-1 ami-9cb80f9d aki-d209a2d3 paravirtual
3natty server daily 20111201 instance-store amd64 ap-northeast-1 ami-7cb80f7d aki-d409a2d5 paravirtual
4natty server daily 20111201 instance-store i386 ap-northeast-1 ami-68b80f69 aki-d209a2d3 paravirtual
5natty server daily 20111201 ebs amd64 ap-southeast-1 ami-62246130 aki-11d5aa43 paravirtual
6natty server daily 20111201 ebs i386 ap-southeast-1 ami-7e24612c aki-13d5aa41 paravirtual
7natty server daily 20111201 instance-store amd64 ap-southeast-1 ami-74246126 aki-11d5aa43 paravirtual
8natty server daily 20111201 instance-store i386 ap-southeast-1 ami-5e24610c aki-13d5aa41 paravirtual
9natty server daily 20111201 ebs amd64 eu-west-1 ami-431d2137 aki-4feec43b paravirtual
10natty server daily 20111201 ebs i386 eu-west-1 ami-691d211d aki-4deec439 paravirtual
11natty server daily 20111201 instance-store amd64 eu-west-1 ami-891c20fd aki-4feec43b paravirtual
12natty server daily 20111201 instance-store i386 eu-west-1 ami-9b1c20ef aki-4deec439 paravirtual
13natty server daily 20111201 ebs amd64 us-east-1 ami-0fec2766 hvm
14natty server daily 20111201 ebs amd64 us-east-1 ami-59ec2730 aki-427d952b paravirtual
15natty server daily 20111201 ebs i386 us-east-1 ami-95ed26fc aki-407d9529 paravirtual
16natty server daily 20111201 instance-store amd64 us-east-1 ami-1fed2676 aki-427d952b paravirtual
17natty server daily 20111201 instance-store i386 us-east-1 ami-3fed2656 aki-407d9529 paravirtual
18natty server daily 20111201 ebs amd64 us-west-1 ami-c983dc8c aki-9ba0f1de paravirtual
19natty server daily 20111201 ebs i386 us-west-1 ami-3583dc70 aki-99a0f1dc paravirtual
20natty server daily 20111201 instance-store amd64 us-west-1 ami-2f83dc6a aki-9ba0f1de paravirtual
21natty server daily 20111201 instance-store i386 us-west-1 ami-2183dc64 aki-99a0f1dc paravirtual
22natty server daily 20111201 ebs amd64 us-west-2 ami-689d1058 aki-ace26f9c paravirtual
23natty server daily 20111201 ebs i386 us-west-2 ami-649d1054 aki-dce26fec paravirtual
24natty server daily 20111201 instance-store amd64 us-west-2 ami-7e9d104e aki-ace26f9c paravirtual
25natty server daily 20111201 instance-store i386 us-west-2 ami-789d1048 aki-dce26fec paravirtual
026
=== added file 'environs/ec2/images/query/natty/server/released.current.txt'
--- environs/ec2/images/query/natty/server/released.current.txt 1970-01-01 00:00:00 +0000
+++ environs/ec2/images/query/natty/server/released.current.txt 2012-01-18 17:30:30 +0000
@@ -0,0 +1,25 @@
1natty server release 20111003 ebs amd64 ap-northeast-1 ami-02b10503 aki-d409a2d5 paravirtual
2natty server release 20111003 ebs i386 ap-northeast-1 ami-00b10501 aki-d209a2d3 paravirtual
3natty server release 20111003 instance-store amd64 ap-northeast-1 ami-fab004fb aki-d409a2d5 paravirtual
4natty server release 20111003 instance-store i386 ap-northeast-1 ami-f0b004f1 aki-d209a2d3 paravirtual
5natty server release 20111003 ebs amd64 ap-southeast-1 ami-04255f56 aki-11d5aa43 paravirtual
6natty server release 20111003 ebs i386 ap-southeast-1 ami-06255f54 aki-13d5aa41 paravirtual
7natty server release 20111003 instance-store amd64 ap-southeast-1 ami-7a255f28 aki-11d5aa43 paravirtual
8natty server release 20111003 instance-store i386 ap-southeast-1 ami-72255f20 aki-13d5aa41 paravirtual
9natty server release 20111003 ebs amd64 eu-west-1 ami-a6f7c5d2 aki-4feec43b paravirtual
10natty server release 20111003 ebs i386 eu-west-1 ami-a4f7c5d0 aki-4deec439 paravirtual
11natty server release 20111003 instance-store amd64 eu-west-1 ami-c0f7c5b4 aki-4feec43b paravirtual
12natty server release 20111003 instance-store i386 eu-west-1 ami-fef7c58a aki-4deec439 paravirtual
13natty server release 20111003 ebs amd64 us-east-1 ami-f1589598 hvm
14natty server release 20111003 ebs amd64 us-east-1 ami-fd589594 aki-427d952b paravirtual
15natty server release 20111003 ebs i386 us-east-1 ami-e358958a aki-407d9529 paravirtual
16natty server release 20111003 instance-store amd64 us-east-1 ami-71589518 aki-427d952b paravirtual
17natty server release 20111003 instance-store i386 us-east-1 ami-c15994a8 aki-407d9529 paravirtual
18natty server release 20111003 ebs amd64 us-west-1 ami-4d580408 aki-9ba0f1de paravirtual
19natty server release 20111003 ebs i386 us-west-1 ami-43580406 aki-99a0f1dc paravirtual
20natty server release 20111003 instance-store amd64 us-west-1 ami-a15f03e4 aki-9ba0f1de paravirtual
21natty server release 20111003 instance-store i386 us-west-1 ami-e95f03ac aki-99a0f1dc paravirtual
22natty server release 20111003 ebs amd64 us-west-2 ami-1af9742a aki-ace26f9c paravirtual
23natty server release 20111003 ebs i386 us-west-2 ami-18f97428 aki-dce26fec paravirtual
24natty server release 20111003 instance-store amd64 us-west-2 ami-16f67b26 aki-ace26f9c paravirtual
25natty server release 20111003 instance-store i386 us-west-2 ami-10f67b20 aki-dce26fec paravirtual
026
=== added directory 'environs/ec2/images/query/oneiric'
=== added directory 'environs/ec2/images/query/oneiric/desktop'
=== added file 'environs/ec2/images/query/oneiric/desktop/daily.current.txt'
--- environs/ec2/images/query/oneiric/desktop/daily.current.txt 1970-01-01 00:00:00 +0000
+++ environs/ec2/images/query/oneiric/desktop/daily.current.txt 2012-01-18 17:30:30 +0000
@@ -0,0 +1,25 @@
1oneiric desktop daily 20111120 ebs amd64 ap-northeast-1 ami-eaec5beb aki-ee5df7ef paravirtual
2oneiric desktop daily 20111120 ebs i386 ap-northeast-1 ami-d4ec5bd5 aki-ec5df7ed paravirtual
3oneiric desktop daily 20111120 instance-store amd64 ap-northeast-1 ami-c0ec5bc1 aki-ee5df7ef paravirtual
4oneiric desktop daily 20111120 instance-store i386 ap-northeast-1 ami-9eec5b9f aki-ec5df7ed paravirtual
5oneiric desktop daily 20111120 ebs amd64 ap-southeast-1 ami-48c9b31a aki-aa225af8 paravirtual
6oneiric desktop daily 20111120 ebs i386 ap-southeast-1 ami-58c9b30a aki-a4225af6 paravirtual
7oneiric desktop daily 20111120 instance-store amd64 ap-southeast-1 ami-b8c8b2ea aki-aa225af8 paravirtual
8oneiric desktop daily 20111120 instance-store i386 ap-southeast-1 ami-9ec8b2cc aki-a4225af6 paravirtual
9oneiric desktop daily 20111120 ebs amd64 eu-west-1 ami-817b47f5 aki-62695816 paravirtual
10oneiric desktop daily 20111120 ebs i386 eu-west-1 ami-af7b47db aki-64695810 paravirtual
11oneiric desktop daily 20111120 instance-store amd64 eu-west-1 ami-137b4767 aki-62695816 paravirtual
12oneiric desktop daily 20111120 instance-store i386 eu-west-1 ami-6f7b471b aki-64695810 paravirtual
13oneiric desktop daily 20111120 ebs amd64 us-east-1 ami-abed25c2 hvm
14oneiric desktop daily 20111120 ebs amd64 us-east-1 ami-dded25b4 aki-825ea7eb paravirtual
15oneiric desktop daily 20111120 ebs i386 us-east-1 ami-f9ed2590 aki-805ea7e9 paravirtual
16oneiric desktop daily 20111120 instance-store amd64 us-east-1 ami-6dec2404 aki-825ea7eb paravirtual
17oneiric desktop daily 20111120 instance-store i386 us-east-1 ami-53eb233a aki-805ea7e9 paravirtual
18oneiric desktop daily 20111120 ebs amd64 us-west-1 ami-7b421d3e aki-8d396bc8 paravirtual
19oneiric desktop daily 20111120 ebs i386 us-west-1 ami-79421d3c aki-83396bc6 paravirtual
20oneiric desktop daily 20111120 instance-store amd64 us-west-1 ami-8b431cce aki-8d396bc8 paravirtual
21oneiric desktop daily 20111120 instance-store i386 us-west-1 ami-e7431ca2 aki-83396bc6 paravirtual
22oneiric desktop daily 20111120 ebs amd64 us-west-2 ami-be89048e aki-98e26fa8 paravirtual
23oneiric desktop daily 20111120 ebs i386 us-west-2 ami-bc89048c aki-c2e26ff2 paravirtual
24oneiric desktop daily 20111120 instance-store amd64 us-west-2 ami-48890478 aki-98e26fa8 paravirtual
25oneiric desktop daily 20111120 instance-store i386 us-west-2 ami-58890468 aki-c2e26ff2 paravirtual
026
=== added directory 'environs/ec2/images/query/oneiric/server'
=== added file 'environs/ec2/images/query/oneiric/server/daily.current.txt'
--- environs/ec2/images/query/oneiric/server/daily.current.txt 1970-01-01 00:00:00 +0000
+++ environs/ec2/images/query/oneiric/server/daily.current.txt 2012-01-18 17:30:30 +0000
@@ -0,0 +1,25 @@
1oneiric server daily 20111205 ebs amd64 ap-northeast-1 ami-42942343 aki-ee5df7ef paravirtual
2oneiric server daily 20111205 ebs i386 ap-northeast-1 ami-3a94233b aki-ec5df7ed paravirtual
3oneiric server daily 20111205 instance-store amd64 ap-northeast-1 ami-24942325 aki-ee5df7ef paravirtual
4oneiric server daily 20111205 instance-store i386 ap-northeast-1 ami-20942321 aki-ec5df7ed paravirtual
5oneiric server daily 20111205 ebs amd64 ap-southeast-1 ami-9c3673ce aki-aa225af8 paravirtual
6oneiric server daily 20111205 ebs i386 ap-southeast-1 ami-9e3673cc aki-a4225af6 paravirtual
7oneiric server daily 20111205 instance-store amd64 ap-southeast-1 ami-fc3673ae aki-aa225af8 paravirtual
8oneiric server daily 20111205 instance-store i386 ap-southeast-1 ami-f83673aa aki-a4225af6 paravirtual
9oneiric server daily 20111205 ebs amd64 eu-west-1 ami-87f9c5f3 aki-62695816 paravirtual
10oneiric server daily 20111205 ebs i386 eu-west-1 ami-aff9c5db aki-64695810 paravirtual
11oneiric server daily 20111205 instance-store amd64 eu-west-1 ami-b5f9c5c1 aki-62695816 paravirtual
12oneiric server daily 20111205 instance-store i386 eu-west-1 ami-d5f9c5a1 aki-64695810 paravirtual
13oneiric server daily 20111205 ebs amd64 us-east-1 ami-015d9668 hvm
14oneiric server daily 20111205 ebs amd64 us-east-1 ami-2f5d9646 aki-825ea7eb paravirtual
15oneiric server daily 20111205 ebs i386 us-east-1 ami-695d9600 aki-805ea7e9 paravirtual
16oneiric server daily 20111205 instance-store amd64 us-east-1 ami-c95e95a0 aki-825ea7eb paravirtual
17oneiric server daily 20111205 instance-store i386 us-east-1 ami-4b5e9522 aki-805ea7e9 paravirtual
18oneiric server daily 20111205 ebs amd64 us-west-1 ami-bba7f8fe aki-8d396bc8 paravirtual
19oneiric server daily 20111205 ebs i386 us-west-1 ami-b3a7f8f6 aki-83396bc6 paravirtual
20oneiric server daily 20111205 instance-store amd64 us-west-1 ami-9da7f8d8 aki-8d396bc8 paravirtual
21oneiric server daily 20111205 instance-store i386 us-west-1 ami-85a7f8c0 aki-83396bc6 paravirtual
22oneiric server daily 20111205 ebs amd64 us-west-2 ami-169b1626 aki-98e26fa8 paravirtual
23oneiric server daily 20111205 ebs i386 us-west-2 ami-109b1620 aki-c2e26ff2 paravirtual
24oneiric server daily 20111205 instance-store amd64 us-west-2 ami-209b1610 aki-98e26fa8 paravirtual
25oneiric server daily 20111205 instance-store i386 us-west-2 ami-3c9b160c aki-c2e26ff2 paravirtual
026
=== added file 'environs/ec2/images/query/oneiric/server/released.current.txt'
--- environs/ec2/images/query/oneiric/server/released.current.txt 1970-01-01 00:00:00 +0000
+++ environs/ec2/images/query/oneiric/server/released.current.txt 2012-01-18 17:30:30 +0000
@@ -0,0 +1,25 @@
1oneiric server release 20111011 ebs amd64 ap-northeast-1 ami-30902431 aki-ee5df7ef paravirtual
2oneiric server release 20111011 ebs i386 ap-northeast-1 ami-2e90242f aki-ec5df7ed paravirtual
3oneiric server release 20111011 instance-store amd64 ap-northeast-1 ami-fa9723fb aki-ee5df7ef paravirtual
4oneiric server release 20111011 instance-store i386 ap-northeast-1 ami-e49723e5 aki-ec5df7ed paravirtual
5oneiric server release 20111011 ebs amd64 ap-southeast-1 ami-7a057f28 aki-aa225af8 paravirtual
6oneiric server release 20111011 ebs i386 ap-southeast-1 ami-76057f24 aki-a4225af6 paravirtual
7oneiric server release 20111011 instance-store amd64 ap-southeast-1 ami-54057f06 aki-aa225af8 paravirtual
8oneiric server release 20111011 instance-store i386 ap-southeast-1 ami-82047ed0 aki-a4225af6 paravirtual
9oneiric server release 20111011 ebs amd64 eu-west-1 ami-61b28015 aki-62695816 paravirtual
10oneiric server release 20111011 ebs i386 eu-west-1 ami-65b28011 aki-64695810 paravirtual
11oneiric server release 20111011 instance-store amd64 eu-west-1 ami-75b28001 aki-62695816 paravirtual
12oneiric server release 20111011 instance-store i386 eu-west-1 ami-dfcdffab aki-64695810 paravirtual
13oneiric server release 20111011 ebs amd64 us-east-1 ami-bff539d6 hvm
14oneiric server release 20111011 ebs amd64 us-east-1 ami-bbf539d2 aki-825ea7eb paravirtual
15oneiric server release 20111011 ebs i386 us-east-1 ami-a7f539ce aki-805ea7e9 paravirtual
16oneiric server release 20111011 instance-store amd64 us-east-1 ami-21f53948 aki-825ea7eb paravirtual
17oneiric server release 20111011 instance-store i386 us-east-1 ami-29f43840 aki-805ea7e9 paravirtual
18oneiric server release 20111011 ebs amd64 us-west-1 ami-7b772b3e aki-8d396bc8 paravirtual
19oneiric server release 20111011 ebs i386 us-west-1 ami-79772b3c aki-83396bc6 paravirtual
20oneiric server release 20111011 instance-store amd64 us-west-1 ami-4b772b0e aki-8d396bc8 paravirtual
21oneiric server release 20111011 instance-store i386 us-west-1 ami-a7762ae2 aki-83396bc6 paravirtual
22oneiric server release 20111011 ebs amd64 us-west-2 ami-2af9741a aki-98e26fa8 paravirtual
23oneiric server release 20111011 ebs i386 us-west-2 ami-20f97410 aki-c2e26ff2 paravirtual
24oneiric server release 20111011 instance-store amd64 us-west-2 ami-56f67b66 aki-98e26fa8 paravirtual
25oneiric server release 20111011 instance-store i386 us-west-2 ami-52f67b62 aki-c2e26ff2 paravirtual
026
=== added file 'environs/ec2/live_test.go'
--- environs/ec2/live_test.go 1970-01-01 00:00:00 +0000
+++ environs/ec2/live_test.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,34 @@
1package ec2_test
2
3import (
4 "fmt"
5 . "launchpad.net/gocheck"
6 "launchpad.net/juju/go/environs"
7 "launchpad.net/juju/go/environs/jujutest"
8)
9
10// integrationConfig holds the environments configuration
11// for running the amazon EC2 integration tests.
12//
13// This is missing keys for security reasons; set the following environment variables
14// to make the integration testing work:
15// access-key: $AWS_ACCESS_KEY_ID
16// admin-secret: $AWS_SECRET_ACCESS_KEY
17var integrationConfig = []byte(`
18environments:
19 sample:
20 type: ec2
21`)
22
23func registerIntegrationTests() {
24 envs, err := environs.ReadEnvironsBytes(integrationConfig)
25 if err != nil {
26 panic(fmt.Errorf("cannot parse integration tests config data: %v", err))
27 }
28 for _, name := range envs.Names() {
29 Suite(&jujutest.LiveTests{
30 Environs: envs,
31 Name: name,
32 })
33 }
34}
035
=== added file 'environs/ec2/local_test.go'
--- environs/ec2/local_test.go 1970-01-01 00:00:00 +0000
+++ environs/ec2/local_test.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,242 @@
1package ec2_test
2
3import (
4 "fmt"
5 "launchpad.net/goamz/aws"
6 amzec2 "launchpad.net/goamz/ec2"
7 "launchpad.net/goamz/ec2/ec2test"
8 . "launchpad.net/gocheck"
9 "launchpad.net/juju/go/environs"
10 "launchpad.net/juju/go/environs/ec2"
11 "launchpad.net/juju/go/environs/jujutest"
12)
13
14var functionalConfig = []byte(`
15environments:
16 sample:
17 type: ec2
18 region: test
19`)
20
21// localTests wraps jujutest.Tests by adding
22// set up and tear down functions that start a new
23// ec2test server for each test.
24// The server is accessed by using the "test" region,
25// which is changed to point to the network address
26// of the local server.
27type localTests struct {
28 *jujutest.Tests
29 srv localServer
30}
31
32// localLiveTests performs the live test suite, but locally.
33type localLiveTests struct {
34 *jujutest.LiveTests
35 srv localServer
36}
37
38type localServer struct {
39 srv *ec2test.Server
40 setup func(*ec2test.Server)
41}
42
43// Each test is run in each of the following scenarios.
44// A scenario is implemented by mutating the ec2test
45// server after it starts.
46var scenarios = []struct {
47 name string
48 setup func(*ec2test.Server)
49}{
50 {"normal", normalScenario},
51 {"initial-state-running", initialStateRunningScenario},
52 {"extra-instances", extraInstancesScenario},
53}
54
55func normalScenario(*ec2test.Server) {
56}
57
58func initialStateRunningScenario(srv *ec2test.Server) {
59 srv.SetInitialInstanceState(ec2test.Running)
60}
61
62func extraInstancesScenario(srv *ec2test.Server) {
63 states := []amzec2.InstanceState{
64 ec2test.ShuttingDown,
65 ec2test.Terminated,
66 ec2test.Stopped,
67 }
68 for _, state := range states {
69 srv.NewInstances(1, "m1.small", "ami-a7f539ce", state, nil)
70 }
71}
72
73func registerLocalTests() {
74 ec2.Regions["test"] = aws.Region{}
75 envs, err := environs.ReadEnvironsBytes(functionalConfig)
76 if err != nil {
77 panic(fmt.Errorf("cannot parse functional tests config data: %v", err))
78 }
79
80 for _, name := range envs.Names() {
81 for _, scen := range scenarios {
82 Suite(&localTests{
83 srv: localServer{setup: scen.setup},
84 Tests: &jujutest.Tests{
85 Environs: envs,
86 Name: name,
87 },
88 })
89 Suite(&localLiveTests{
90 srv: localServer{setup: scen.setup},
91 LiveTests: &jujutest.LiveTests{
92 Environs: envs,
93 Name: name,
94 },
95 })
96 }
97 }
98}
99
100func (t *localTests) TestInstanceGroups(c *C) {
101 env, err := t.Environs.Open(t.Name)
102 c.Assert(err, IsNil)
103
104 ec2conn := amzec2.New(aws.Auth{}, ec2.Regions["test"])
105
106 groups := amzec2.SecurityGroupNames(
107 fmt.Sprintf("juju-%s", t.Name),
108 fmt.Sprintf("juju-%s-%d", t.Name, 98),
109 fmt.Sprintf("juju-%s-%d", t.Name, 99),
110 )
111
112 inst0, err := env.StartInstance(98)
113 c.Assert(err, IsNil)
114 defer env.StopInstances([]environs.Instance{inst0})
115
116 // create a same-named group for the second instance
117 // before starting it, to check that it's deleted and
118 // recreated correctly.
119 oldGroup := ensureGroupExists(c, ec2conn, groups[2], "old group")
120
121 inst1, err := env.StartInstance(99)
122 c.Assert(err, IsNil)
123 defer env.StopInstances([]environs.Instance{inst1})
124
125 // go behind the scenes to check the machines have
126 // been put into the correct groups.
127
128 // first check that the old group has been deleted
129 groupsResp, err := ec2conn.SecurityGroups([]amzec2.SecurityGroup{oldGroup}, nil)
130 c.Assert(err, IsNil)
131 c.Check(len(groupsResp.Groups), Equals, 0)
132
133 // then check that the groups have been created.
134 groupsResp, err = ec2conn.SecurityGroups(groups, nil)
135 c.Assert(err, IsNil)
136 c.Assert(len(groupsResp.Groups), Equals, len(groups))
137
138 // for each group, check that it exists and record its id.
139 for i, group := range groups {
140 found := false
141 for _, g := range groupsResp.Groups {
142 if g.Name == group.Name {
143 groups[i].Id = g.Id
144 found = true
145 break
146 }
147 }
148 if !found {
149 c.Fatalf("group %q not found", group.Name)
150 }
151 }
152
153 // check that each instance is part of the correct groups.
154 resp, err := ec2conn.Instances([]string{inst0.Id(), inst1.Id()}, nil)
155 c.Assert(err, IsNil)
156 c.Assert(len(resp.Reservations), Equals, 2, Bug("reservations %#v", resp.Reservations))
157 for _, r := range resp.Reservations {
158 c.Assert(len(r.Instances), Equals, 1)
159 // each instance must be part of the general juju group.
160 msg := Bug("reservation %#v", r)
161 c.Assert(hasSecurityGroup(r, groups[0]), Equals, true, msg)
162 inst := r.Instances[0]
163 switch inst.InstanceId {
164 case inst0.Id():
165 c.Assert(hasSecurityGroup(r, groups[1]), Equals, true, msg)
166 c.Assert(hasSecurityGroup(r, groups[2]), Equals, false, msg)
167 case inst1.Id():
168 c.Assert(hasSecurityGroup(r, groups[2]), Equals, true, msg)
169
170 // check that the id of the second machine's group
171 // has changed - this implies that StartInstance has
172 // correctly deleted and re-created the group.
173 c.Assert(groups[2].Id, Not(Equals), oldGroup.Id)
174 c.Assert(hasSecurityGroup(r, groups[1]), Equals, false, msg)
175 default:
176 c.Errorf("unknown instance found: %v", inst)
177 }
178 }
179}
180
181// createGroup creates a new EC2 group if it doesn't already
182// exist, and returns full SecurityGroup.
183func ensureGroupExists(c *C, ec2conn *amzec2.EC2, group amzec2.SecurityGroup, descr string) amzec2.SecurityGroup {
184 groups, err := ec2conn.SecurityGroups([]amzec2.SecurityGroup{group}, nil)
185 c.Assert(err, IsNil)
186 if len(groups.Groups) > 0 {
187 return groups.Groups[0].SecurityGroup
188 }
189
190 resp, err := ec2conn.CreateSecurityGroup(group.Name, descr)
191 c.Assert(err, IsNil)
192
193 return resp.SecurityGroup
194}
195
196func hasSecurityGroup(r amzec2.Reservation, g amzec2.SecurityGroup) bool {
197 for _, rg := range r.SecurityGroups {
198 if rg.Id == g.Id {
199 return true
200 }
201 }
202 return false
203}
204
205func (t *localTests) SetUpTest(c *C) {
206 t.srv.startServer(c)
207 t.Tests.SetUpTest(c)
208}
209
210func (t *localTests) TearDownTest(c *C) {
211 t.Tests.TearDownTest(c)
212 t.srv.stopServer(c)
213}
214
215func (t *localLiveTests) SetUpSuite(c *C) {
216 t.srv.startServer(c)
217 t.LiveTests.SetUpSuite(c)
218}
219
220func (t *localLiveTests) TearDownSuite(c *C) {
221 t.srv.stopServer(c)
222 t.LiveTests.TearDownSuite(c)
223}
224
225func (srv *localServer) startServer(c *C) {
226 var err error
227 srv.srv, err = ec2test.NewServer()
228 if err != nil {
229 c.Fatalf("cannot start ec2 test server: %v", err)
230 }
231 ec2.Regions["test"] = aws.Region{
232 EC2Endpoint: srv.srv.Address(),
233 }
234 srv.setup(srv.srv)
235}
236
237func (srv *localServer) stopServer(c *C) {
238 srv.srv.Quit()
239 // Clear out the region because the server address is
240 // no longer valid.
241 ec2.Regions["test"] = aws.Region{}
242}
0243
=== added file 'environs/ec2/suite_test.go'
--- environs/ec2/suite_test.go 1970-01-01 00:00:00 +0000
+++ environs/ec2/suite_test.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,25 @@
1package ec2_test
2
3import (
4 "flag"
5 . "launchpad.net/gocheck"
6 "testing"
7)
8
9type suite struct{}
10
11var _ = Suite(suite{})
12
13var regenerate = flag.Bool("regenerate-images", false, "regenerate all data in images directory")
14var integration = flag.Bool("i", false, "Enable integration tests")
15
16func TestEC2(t *testing.T) {
17 if *regenerate {
18 regenerateImages(t)
19 }
20 if *integration {
21 registerIntegrationTests()
22 }
23 registerLocalTests()
24 TestingT(t)
25}
026
=== added file 'environs/ec2/util.go'
--- environs/ec2/util.go 1970-01-01 00:00:00 +0000
+++ environs/ec2/util.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,43 @@
1package ec2
2
3import (
4 "launchpad.net/juju/go/schema"
5)
6
7// this stuff could/should be in the schema package.
8
9// checkerFunc defines a schema.Checker using a function that
10// implemenets scheme.Checker.Coerce.
11type checkerFunc func(v interface{}, path []string) (newv interface{}, err error)
12
13func (f checkerFunc) Coerce(v interface{}, path []string) (newv interface{}, err error) {
14 return f(v, path)
15}
16
17// combineCheckers returns a Checker that checks a value by passing
18// it through the "pipeline" defined by checkers. When
19// the returned checker's Coerce method is called on a value,
20// the value is passed through the first checker in checkers;
21// the resulting value is used as input to the next checker, and so on.
22func combineCheckers(checkers ...schema.Checker) schema.Checker {
23 f := func(v interface{}, path []string) (newv interface{}, err error) {
24 for _, c := range checkers {
25 v, err = c.Coerce(v, path)
26 if err != nil {
27 return nil, err
28 }
29 }
30 return v, nil
31 }
32 return checkerFunc(f)
33}
34
35// oneOf(a, b, c) is equivalent to (but less verbose than):
36// schema.OneOf(schema.Const(a), schema.Const(b), schema.Const(c))
37func oneOf(values ...interface{}) schema.Checker {
38 c := make([]schema.Checker, len(values))
39 for i, v := range values {
40 c[i] = schema.Const(v)
41 }
42 return schema.OneOf(c...)
43}
044
=== added file 'environs/interface.go'
--- environs/interface.go 1970-01-01 00:00:00 +0000
+++ environs/interface.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,42 @@
1package environs
2
3import "launchpad.net/juju/go/schema"
4
5// A EnvironProvider represents a computing and storage provider.
6type EnvironProvider interface {
7 // ConfigChecker is used to check sections of the environments.yaml
8 // file that specify this provider. The value passed to the Checker is
9 // that returned from the yaml parse, of type schema.MapType.
10 ConfigChecker() schema.Checker
11
12 // NewEnviron creates a new Environ with
13 // the given attributes returned by the ConfigChecker.
14 // The name is that given in environments.yaml.
15 Open(name string, attributes interface{}) (Environ, error)
16}
17
18// Instance represents the provider-specific notion of a machine.
19type Instance interface {
20 // Id returns a provider-generated identifier for the Instance.
21 Id() string
22 DNSName() string
23}
24
25// An Environ represents a juju environment as specified
26// in the environments.yaml file.
27type Environ interface {
28 // StartInstance asks for a new instance to be created,
29 // associated with the provided machine identifier
30 // TODO add arguments to specify type of new machine.
31 StartInstance(machineId int) (Instance, error)
32
33 // StopInstances shuts down the given instances.
34 StopInstances([]Instance) error
35
36 // Instances returns the list of currently started instances.
37 Instances() ([]Instance, error)
38
39 // Destroy shuts down all known machines and destroys the
40 // rest of the environment.
41 Destroy() error
42}
043
=== added directory 'environs/jujutest'
=== added file 'environs/jujutest/Makefile'
--- environs/jujutest/Makefile 1970-01-01 00:00:00 +0000
+++ environs/jujutest/Makefile 2012-01-18 17:30:30 +0000
@@ -0,0 +1,25 @@
1include $(GOROOT)/src/Make.inc
2
3all: package
4
5TARG=launchpad.net/juju/go/environs/jujutest
6
7GOFILES=\
8 test.go\
9 tests.go\
10 livetests.go\
11
12GOFMT=gofmt
13BADFMT:=$(shell $(GOFMT) -l $(GOFILES) $(CGOFILES) $(wildcard *_test.go))
14
15gofmt: $(BADFMT)
16 @for F in $(BADFMT); do $(GOFMT) -w $$F && echo $$F; done
17
18ifneq ($(BADFMT),)
19ifneq ($(MAKECMDGOALS),gofmt)
20$(warning WARNING: make gofmt: $(BADFMT))
21endif
22endif
23
24include $(GOROOT)/src/Make.pkg
25
026
=== added file 'environs/jujutest/jujutest_test.go'
--- environs/jujutest/jujutest_test.go 1970-01-01 00:00:00 +0000
+++ environs/jujutest/jujutest_test.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,8 @@
1package jujutest
2
3import "testing"
4
5// A dummy test so that gotest succeeds when running
6// in this directory.
7func TestNothing(t *testing.T) {
8}
09
=== added file 'environs/jujutest/livetests.go'
--- environs/jujutest/livetests.go 1970-01-01 00:00:00 +0000
+++ environs/jujutest/livetests.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,52 @@
1package jujutest
2
3import (
4 . "launchpad.net/gocheck"
5 "launchpad.net/juju/go/environs"
6)
7
8// TestStartStop is similar to Tests.TestStartStop except
9// that it does not assume a pristine environment.
10func (t *LiveTests) TestStartStop(c *C) {
11 names := make(map[string]environs.Instance)
12 insts, err := t.env.Instances()
13 c.Assert(err, IsNil)
14
15 // check there are no duplicate instance ids
16 for _, inst := range insts {
17 id := inst.Id()
18 c.Assert(names[id], IsNil)
19 names[id] = inst
20 }
21
22 inst, err := t.env.StartInstance(0)
23 c.Assert(err, IsNil)
24 c.Assert(inst, NotNil)
25 id0 := inst.Id()
26
27 insts, err = t.env.Instances()
28 c.Assert(err, IsNil)
29
30 // check the new instance is found
31 found := false
32 for _, inst := range insts {
33 if inst.Id() == id0 {
34 c.Assert(found, Equals, false)
35 found = true
36 }
37 }
38 c.Check(found, Equals, true)
39
40 err = t.env.StopInstances([]environs.Instance{inst})
41 c.Assert(err, IsNil)
42
43 insts, err = t.env.Instances()
44 c.Assert(err, IsNil)
45 c.Assert(len(insts), Equals, 0)
46
47 // check the instance is no longer there.
48 found = true
49 for _, inst := range insts {
50 c.Assert(inst.Id(), Not(Equals), id0)
51 }
52}
053
=== added file 'environs/jujutest/test.go'
--- environs/jujutest/test.go 1970-01-01 00:00:00 +0000
+++ environs/jujutest/test.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,66 @@
1package jujutest
2
3import (
4 . "launchpad.net/gocheck"
5 "launchpad.net/juju/go/environs"
6)
7
8// Tests is a gocheck suite containing tests verifying
9// juju functionality against the environment with Name that
10// must exist within Environs.
11type Tests struct {
12 Environs *environs.Environs
13 Name string
14
15 environs []environs.Environ
16}
17
18func (t *Tests) open(c *C) environs.Environ {
19 e, err := t.Environs.Open(t.Name)
20 c.Assert(err, IsNil, Bug("opening environ %q", t.Name))
21 c.Assert(e, NotNil)
22 t.environs = append(t.environs, e)
23 return e
24}
25
26func (t *Tests) SetUpSuite(*C) {
27}
28
29func (t *Tests) TearDownSuite(*C) {
30}
31
32func (t *Tests) SetUpTest(*C) {
33}
34
35func (t *Tests) TearDownTest(c *C) {
36 for _, e := range t.environs {
37 err := e.Destroy()
38 if err != nil {
39 c.Errorf("error destroying environment after test: %v", err)
40 }
41 }
42 t.environs = nil
43}
44
45type LiveTests struct {
46 Environs *environs.Environs
47 Name string
48 env environs.Environ
49}
50
51func (t *LiveTests) SetUpSuite(c *C) {
52 e, err := t.Environs.Open(t.Name)
53 c.Assert(err, IsNil, Bug("opening environ %q", t.Name))
54 c.Assert(e, NotNil)
55 t.env = e
56}
57
58func (t *LiveTests) TearDownSuite(c *C) {
59 t.env = nil
60}
61
62func (t *LiveTests) SetUpTest(*C) {
63}
64
65func (t *LiveTests) TearDownTest(*C) {
66}
067
=== added file 'environs/jujutest/tests.go'
--- environs/jujutest/tests.go 1970-01-01 00:00:00 +0000
+++ environs/jujutest/tests.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,31 @@
1package jujutest
2
3import (
4 . "launchpad.net/gocheck"
5 "launchpad.net/juju/go/environs"
6)
7
8func (t *Tests) TestStartStop(c *C) {
9 e := t.open(c)
10
11 insts, err := e.Instances()
12 c.Assert(err, IsNil)
13 c.Assert(len(insts), Equals, 0)
14
15 inst, err := e.StartInstance(0)
16 c.Assert(err, IsNil)
17 c.Assert(inst, NotNil)
18 id0 := inst.Id()
19
20 insts, err = e.Instances()
21 c.Assert(err, IsNil)
22 c.Assert(len(insts), Equals, 1)
23 c.Assert(insts[0].Id(), Equals, id0)
24
25 err = e.StopInstances([]environs.Instance{inst})
26 c.Assert(err, IsNil)
27
28 insts, err = e.Instances()
29 c.Assert(err, IsNil)
30 c.Assert(len(insts), Equals, 0)
31}
032
=== added file 'environs/open.go'
--- environs/open.go 1970-01-01 00:00:00 +0000
+++ environs/open.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,28 @@
1package environs
2
3import "fmt"
4
5// New creates a new Environ using the
6// environment configuration with the given name.
7// If name is empty, the default environment will be used.
8func (envs *Environs) Open(name string) (Environ, error) {
9 if name == "" {
10 name = envs.Default
11 if name == "" {
12 return nil, fmt.Errorf("no default environment found")
13 }
14 }
15 e, ok := envs.environs[name]
16 if !ok {
17 return nil, fmt.Errorf("unknown environment %q", name)
18 }
19 if e.err != nil {
20 return nil, e.err
21 }
22 env, err := providers[e.kind].Open(name, e.config)
23 if err != nil {
24 return nil, fmt.Errorf("cannot initialize environment %q: %v", name, err)
25 }
26
27 return env, nil
28}
029
=== added file 'environs/suite_test.go'
--- environs/suite_test.go 1970-01-01 00:00:00 +0000
+++ environs/suite_test.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,14 @@
1package environs_test
2
3import (
4 . "launchpad.net/gocheck"
5 "testing"
6)
7
8func Test(t *testing.T) {
9 TestingT(t)
10}
11
12type suite struct{}
13
14var _ = Suite(suite{})
015
=== added directory 'log'
=== added file 'log/Makefile'
--- log/Makefile 1970-01-01 00:00:00 +0000
+++ log/Makefile 2012-01-18 17:30:30 +0000
@@ -0,0 +1,23 @@
1include $(GOROOT)/src/Make.inc
2
3all: package
4
5TARG=launchpad.net/juju/go/log
6
7GOFILES=\
8 log.go\
9
10GOFMT=gofmt
11BADFMT:=$(shell $(GOFMT) -l $(GOFILES) $(CGOFILES) $(wildcard *_test.go))
12
13gofmt: $(BADFMT)
14 @for F in $(BADFMT); do $(GOFMT) -w $$F && echo $$F; done
15
16ifneq ($(BADFMT),)
17ifneq ($(MAKECMDGOALS),gofmt)
18$(warning WARNING: make gofmt: $(BADFMT))
19endif
20endif
21
22include $(GOROOT)/src/Make.pkg
23
024
=== added file 'log/log.go'
--- log/log.go 1970-01-01 00:00:00 +0000
+++ log/log.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,27 @@
1package log
2
3import "fmt"
4
5type Logger interface {
6 Output(calldepth int, s string) error
7}
8
9var (
10 Target Logger
11 Debug bool
12)
13
14// Printf logs the formatted message onto the Target Logger.
15func Printf(format string, v ...interface{}) {
16 if Target != nil {
17 Target.Output(2, "JUJU "+fmt.Sprintf(format, v...))
18 }
19}
20
21// Debugf logs the formatted message onto the Target Logger
22// if Debug is true.
23func Debugf(format string, v ...interface{}) {
24 if Debug && Target != nil {
25 Target.Output(2, "JUJU:DEBUG "+fmt.Sprintf(format, v...))
26 }
27}
028
=== added file 'log/log_test.go'
--- log/log_test.go 1970-01-01 00:00:00 +0000
+++ log/log_test.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,54 @@
1package log_test
2
3import (
4 "bytes"
5 . "launchpad.net/gocheck"
6 "launchpad.net/juju/go/log"
7 stdlog "log"
8 "testing"
9)
10
11func Test(t *testing.T) {
12 TestingT(t)
13}
14
15type suite struct{}
16
17var _ = Suite(suite{})
18
19type logTest struct {
20 input string
21 debug bool
22}
23
24var logTests = []struct {
25 input string
26 debug bool
27}{
28 {
29 input: "Hello World",
30 debug: false,
31 },
32 {
33 input: "Hello World",
34 debug: true,
35 },
36}
37
38func (suite) TestLogger(c *C) {
39 buf := &bytes.Buffer{}
40 log.Target = stdlog.New(buf, "", 0)
41 for _, t := range logTests {
42 log.Debug = t.debug
43 log.Printf(t.input)
44 c.Assert(buf.String(), Equals, "JUJU "+t.input+"\n")
45 buf.Reset()
46 log.Debugf(t.input)
47 if t.debug {
48 c.Assert(buf.String(), Equals, "JUJU:DEBUG "+t.input+"\n")
49 } else {
50 c.Assert(buf.String(), Equals, "")
51 }
52 buf.Reset()
53 }
54}
055
=== added directory 'schema'
=== added file 'schema/Makefile'
--- schema/Makefile 1970-01-01 00:00:00 +0000
+++ schema/Makefile 2012-01-18 17:30:30 +0000
@@ -0,0 +1,23 @@
1include $(GOROOT)/src/Make.inc
2
3all: package
4
5TARG=launchpad.net/juju/go/schema
6
7GOFILES=\
8 schema.go\
9
10GOFMT=gofmt
11BADFMT:=$(shell $(GOFMT) -l $(GOFILES) $(CGOFILES) $(wildcard *_test.go))
12
13gofmt: $(BADFMT)
14 @for F in $(BADFMT); do $(GOFMT) -w $$F && echo $$F; done
15
16ifneq ($(BADFMT),)
17ifneq ($(MAKECMDGOALS),gofmt)
18$(warning WARNING: make gofmt: $(BADFMT))
19endif
20endif
21
22include $(GOROOT)/src/Make.pkg
23
024
=== added file 'schema/schema.go'
--- schema/schema.go 1970-01-01 00:00:00 +0000
+++ schema/schema.go 2012-01-18 17:30:30 +0000
@@ -0,0 +1,374 @@
1package schema
2
3import (
4 "fmt"
5 "reflect"
6 "regexp"
7 "strconv"
8 "strings"
9)
10
11// All map types used in the schema package are of type MapType.
12type MapType map[interface{}]interface{}
13
14// All the slice types generated in the schema package are of type ListType.
15type ListType []interface{}
16
17// The Coerce method of the Checker interface is called recursively when
18// v is being validated. If err is nil, newv is used as the new value
19// at the recursion point. If err is non-nil, v is taken as invalid and
20// may be either ignored or error out depending on where in the schema
21// checking process the error happened. Checkers like OneOf may continue
22// with an alternative, for instance.
23type Checker interface {
24 Coerce(v interface{}, path []string) (newv interface{}, err error)
25}
26
27type error_ struct {
28 want string
29 got interface{}
30 path []string
31}
32
33func (e error_) Error() string {
34 var path string
35 if e.path[0] == "." {
36 path = strings.Join(e.path[1:], "")
37 } else {
38 path = strings.Join(e.path, "")
39 }
40 if e.want == "" {
41 return fmt.Sprintf("%s: unsupported value", path)
42 }
43 if e.got == nil {
44 return fmt.Sprintf("%s: expected %s, got nothing", path, e.want)
45 }
46 return fmt.Sprintf("%s: expected %s, got %#v", path, e.want, e.got)
47}
48
49// Any returns a Checker that succeeds with any input value and
50// results in the value itself unprocessed.
51func Any() Checker {
52 return anyC{}
53}
54
55type anyC struct{}
56
57func (c anyC) Coerce(v interface{}, path []string) (interface{}, error) {
58 return v, nil
59}
60
61// Const returns a Checker that only succeeds if the input matches
62// value exactly. The value is compared with reflect.DeepEqual.
63func Const(value interface{}) Checker {
64 return constC{value}
65}
66
67type constC struct {
68 value interface{}
69}
70
71func (c constC) Coerce(v interface{}, path []string) (interface{}, error) {
72 if reflect.DeepEqual(v, c.value) {
73 return v, nil
74 }
75 return nil, error_{fmt.Sprintf("%#v", c.value), v, path}
76}
77
78// OneOf returns a Checker that attempts to Coerce the value with each
79// of the provided checkers. The value returned by the first checker
80// that succeeds will be returned by the OneOf checker itself. If no
81// checker succeeds, OneOf will return an error on coercion.
82func OneOf(options ...Checker) Checker {
83 return oneOfC{options}
84}
85
86type oneOfC struct {
87 options []Checker
88}
89
90func (c oneOfC) Coerce(v interface{}, path []string) (interface{}, error) {
91 for _, o := range c.options {
92 newv, err := o.Coerce(v, path)
93 if err == nil {
94 return newv, nil
95 }
96 }
97 return nil, error_{path: path}
98}
99
100// Bool returns a Checker that accepts boolean values only.
101func Bool() Checker {
102 return boolC{}
103}
104
105type boolC struct{}
106
107func (c boolC) Coerce(v interface{}, path []string) (interface{}, error) {
108 if v != nil && reflect.TypeOf(v).Kind() == reflect.Bool {
109 return v, nil
110 }
111 return nil, error_{"bool", v, path}
112}
113
114// Int returns a Checker that accepts any integer value, and returns
115// the same value consistently typed as an int64.
116func Int() Checker {
117 return intC{}
118}
119
120type intC struct{}
121
122func (c intC) Coerce(v interface{}, path []string) (interface{}, error) {
123 if v == nil {
124 return nil, error_{"int", v, path}
125 }
126 switch reflect.TypeOf(v).Kind() {
127 case reflect.Int:
128 case reflect.Int8:
129 case reflect.Int16:
130 case reflect.Int32:
131 case reflect.Int64:
132 default:
133 return nil, error_{"int", v, path}
134 }
135 return reflect.ValueOf(v).Int(), nil
136}
137
138// Int returns a Checker that accepts any float value, and returns
139// the same value consistently typed as a float64.
140func Float() Checker {
141 return floatC{}
142}
143
144type floatC struct{}
145
146func (c floatC) Coerce(v interface{}, path []string) (interface{}, error) {
147 if v == nil {
148 return nil, error_{"float", v, path}
149 }
150 switch reflect.TypeOf(v).Kind() {
151 case reflect.Float32:
152 case reflect.Float64:
153 default:
154 return nil, error_{"float", v, path}
155 }
156 return reflect.ValueOf(v).Float(), nil
157}
158
159// String returns a Checker that accepts a string value only and returns
160// it unprocessed.
161func String() Checker {
162 return stringC{}
163}
164
165type stringC struct{}
166
167func (c stringC) Coerce(v interface{}, path []string) (interface{}, error) {
168 if v != nil && reflect.TypeOf(v).Kind() == reflect.String {
169 return reflect.ValueOf(v).String(), nil
170 }
171 return nil, error_{"string", v, path}
172}
173
174func SimpleRegexp() Checker {
175 return sregexpC{}
176}
177
178type sregexpC struct{}
179
180func (c sregexpC) Coerce(v interface{}, path []string) (interface{}, error) {
181 // XXX The regexp package happens to be extremely simple right now.
182 // Once exp/regexp goes mainstream, we'll have to update this
183 // logic to use a more widely accepted regexp subset.
184 if v != nil && reflect.TypeOf(v).Kind() == reflect.String {
185 s := reflect.ValueOf(v).String()
186 _, err := regexp.Compile(s)
187 if err != nil {
188 return nil, error_{"valid regexp", s, path}
189 }
190 return v, nil
191 }
192 return nil, error_{"regexp string", v, path}
193}
194
195// List returns a Checker that accepts a slice value with values
196// that are processed with the elem checker. If any element of the
197// provided slice value fails to be processed, processing will stop
198// and return with the obtained error.
199//
200// The coerced output value has type schema.ListType.
201func List(elem Checker) Checker {
202 return listC{elem}
203}
204
205type listC struct {
206 elem Checker
207}
208
209func (c listC) Coerce(v interface{}, path []string) (interface{}, error) {
210 rv := reflect.ValueOf(v)
211 if rv.Kind() != reflect.Slice {
212 return nil, error_{"list", v, path}
213 }
214
215 path = append(path, "[", "?", "]")
216
217 l := rv.Len()
218 out := make(ListType, 0, l)
219 for i := 0; i != l; i++ {
220 path[len(path)-2] = strconv.Itoa(i)
221 elem, err := c.elem.Coerce(rv.Index(i).Interface(), path)
222 if err != nil {
223 return nil, err
224 }
225 out = append(out, elem)
226 }
227 return out, nil
228}
229
230// Map returns a Checker that accepts a map value. Every key and value
231// in the map are processed with the respective checker, and if any
232// value fails to be coerced, processing stops and returns with the
233// underlying error.
234//
235// The coerced output value has type schema.MapType.
236func Map(key Checker, value Checker) Checker {
237 return mapC{key, value}
238}
239
240type mapC struct {
241 key Checker
242 value Checker
243}
244
245func (c mapC) Coerce(v interface{}, path []string) (interface{}, error) {
246 rv := reflect.ValueOf(v)
247 if rv.Kind() != reflect.Map {
248 return nil, error_{"map", v, path}
249 }
250
251 vpath := append(path, ".", "?")
252
253 l := rv.Len()
254 out := make(MapType, l)
255 keys := rv.MapKeys()
256 for i := 0; i != l; i++ {
257 k := keys[i]
258 newk, err := c.key.Coerce(k.Interface(), path)
259 if err != nil {
260 return nil, err
261 }
262 vpath[len(vpath)-1] = fmt.Sprint(k.Interface())
263 newv, err := c.value.Coerce(rv.MapIndex(k).Interface(), vpath)
264 if err != nil {
265 return nil, err
266 }
267 out[newk] = newv
268 }
269 return out, nil
270}
271
272type Fields map[string]Checker
273type Optional []string
274
275// FieldMap returns a Checker that accepts a map value with defined
276// string keys. Every key has an independent checker associated,
277// and processing will only succeed if all the values succeed
278// individually. If a field fails to be processed, processing stops
279// and returns with the underlying error.
280//
281// The coerced output value has type schema.MapType.
282func FieldMap(fields Fields, optional Optional) Checker {
283 return fieldMapC{fields, optional}
284}
285
286type fieldMapC struct {
287 fields Fields
288 optional []string
289}
290
291func (c fieldMapC) isOptional(key string) bool {
292 for _, k := range c.optional {
293 if k == key {
294 return true
295 }
296 }
297 return false
298}
299
300func (c fieldMapC) Coerce(v interface{}, path []string) (interface{}, error) {
301 rv := reflect.ValueOf(v)
302 if rv.Kind() != reflect.Map {
303 return nil, error_{"map", v, path}
304 }
305
306 vpath := append(path, ".", "?")
307
308 l := rv.Len()
309 out := make(MapType, l)
310 for k, checker := range c.fields {
311 vpath[len(vpath)-1] = k
312 var value interface{}
313 valuev := rv.MapIndex(reflect.ValueOf(k))
314 if valuev.IsValid() {
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to status/vote changes: