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