Merge lp:~allenap/juju-core/break-out-juju-store into lp:~juju/juju-core/trunk
- break-out-juju-store
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | William Reade |
Approved revision: | no longer in the source branch. |
Merged at revision: | 886 |
Proposed branch: | lp:~allenap/juju-core/break-out-juju-store |
Merge into: | lp:~juju/juju-core/trunk |
Diff against target: |
2667 lines (+0/-2600) 13 files modified
cmd/charmd/config.yaml (+0/-2) cmd/charmd/main.go (+0/-74) cmd/charmload/config.yaml (+0/-1) cmd/charmload/main.go (+0/-75) store/branch.go (+0/-152) store/branch_test.go (+0/-238) store/lpad.go (+0/-113) store/lpad_test.go (+0/-68) store/mgo_test.go (+0/-95) store/server.go (+0/-191) store/server_test.go (+0/-209) store/store.go (+0/-774) store/store_test.go (+0/-608) |
To merge this branch: | bzr merge lp:~allenap/juju-core/break-out-juju-store |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
The Go Language Gophers | Pending | ||
Review via email: mp+142564@code.launchpad.net |
Commit message
Description of the change
Break out store and cmd/charm* into lp:juju-store
Red Squad are going to be working on the charm store, and it was
suggested that an early task would be to split the charm store out
into a separate project. That work has already been done - see
lp:juju-store - and this is the clean-up job.
mgz helped me a lot in doing both these tasks.
Fwiw, juju-store has not been advertised, so feel free to suggest a
different name.
Gavin Panella (allenap) wrote : | # |
William Reade (fwereade) wrote : | # |
On 2013/01/09 17:59:58, allenap wrote:
> Please take a look.
LGTM; I'm +1 on juju-store myself.
William Reade (fwereade) wrote : | # |
Note that https:/
Gavin Panella (allenap) wrote : | # |
Reopening. This consensus from Austin is that the store should be broken out into a separate project.
William Reade (fwereade) wrote : | # |
As long as the latest version of store is taken, and any bugs are moved, I'm +1 on just going ahead and doing this.
Gavin Panella (allenap) wrote : | # |
William, would you be able to land this please? I don't have the necessary fu. However, before you do, do you know if this will break any service deployment scripts for store.juju.
Gavin Panella (allenap) wrote : | # |
I forgot to mention: lp:juju-store has the latest store code from lp:juju-core, and I'm happy to find and move all the bugs over when this lands.
Preview Diff
1 | === removed directory 'cmd/charmd' |
2 | === removed file 'cmd/charmd/config.yaml' |
3 | --- cmd/charmd/config.yaml 2012-04-02 15:05:40 +0000 |
4 | +++ cmd/charmd/config.yaml 1970-01-01 00:00:00 +0000 |
5 | @@ -1,2 +0,0 @@ |
6 | -mongo-url: localhost:60017 |
7 | -api-addr: localhost:8080 |
8 | |
9 | === removed file 'cmd/charmd/main.go' |
10 | --- cmd/charmd/main.go 2012-06-21 20:40:39 +0000 |
11 | +++ cmd/charmd/main.go 1970-01-01 00:00:00 +0000 |
12 | @@ -1,74 +0,0 @@ |
13 | -package main |
14 | - |
15 | -import ( |
16 | - "fmt" |
17 | - "io/ioutil" |
18 | - "launchpad.net/goyaml" |
19 | - "launchpad.net/juju-core/log" |
20 | - "launchpad.net/juju-core/store" |
21 | - stdlog "log" |
22 | - "net/http" |
23 | - "os" |
24 | - "path/filepath" |
25 | -) |
26 | - |
27 | -func main() { |
28 | - log.Target = stdlog.New(os.Stdout, "", stdlog.LstdFlags) |
29 | - err := serve() |
30 | - if err != nil { |
31 | - fmt.Fprintf(os.Stderr, "%v\n", err) |
32 | - os.Exit(1) |
33 | - } |
34 | -} |
35 | - |
36 | -type config struct { |
37 | - MongoURL string `yaml:"mongo-url"` |
38 | - APIAddr string `yaml:"api-addr"` |
39 | -} |
40 | - |
41 | -func readConfig(path string, conf interface{}) error { |
42 | - f, err := os.Open(path) |
43 | - if err != nil { |
44 | - return fmt.Errorf("opening config file: %v", err) |
45 | - } |
46 | - data, err := ioutil.ReadAll(f) |
47 | - f.Close() |
48 | - if err != nil { |
49 | - return fmt.Errorf("reading config file: %v", err) |
50 | - } |
51 | - err = goyaml.Unmarshal(data, conf) |
52 | - if err != nil { |
53 | - return fmt.Errorf("processing config file: %v", err) |
54 | - } |
55 | - return nil |
56 | -} |
57 | - |
58 | -func serve() error { |
59 | - var confPath string |
60 | - if len(os.Args) == 2 { |
61 | - if _, err := os.Stat(os.Args[1]); err == nil { |
62 | - confPath = os.Args[1] |
63 | - } |
64 | - } |
65 | - if confPath == "" { |
66 | - return fmt.Errorf("usage: %s <config path>", filepath.Base(os.Args[0])) |
67 | - } |
68 | - var conf config |
69 | - err := readConfig(confPath, &conf) |
70 | - if err != nil { |
71 | - return err |
72 | - } |
73 | - if conf.MongoURL == "" || conf.APIAddr == "" { |
74 | - return fmt.Errorf("missing mongo-url or api-addr in config file") |
75 | - } |
76 | - s, err := store.Open(conf.MongoURL) |
77 | - if err != nil { |
78 | - return err |
79 | - } |
80 | - defer s.Close() |
81 | - server, err := store.NewServer(s) |
82 | - if err != nil { |
83 | - return err |
84 | - } |
85 | - return http.ListenAndServe(conf.APIAddr, server) |
86 | -} |
87 | |
88 | === removed directory 'cmd/charmload' |
89 | === removed file 'cmd/charmload/config.yaml' |
90 | --- cmd/charmload/config.yaml 2012-04-02 15:05:40 +0000 |
91 | +++ cmd/charmload/config.yaml 1970-01-01 00:00:00 +0000 |
92 | @@ -1,1 +0,0 @@ |
93 | -mongo-url: localhost:60017 |
94 | |
95 | === removed file 'cmd/charmload/main.go' |
96 | --- cmd/charmload/main.go 2012-06-21 20:40:39 +0000 |
97 | +++ cmd/charmload/main.go 1970-01-01 00:00:00 +0000 |
98 | @@ -1,75 +0,0 @@ |
99 | -package main |
100 | - |
101 | -import ( |
102 | - "fmt" |
103 | - "io/ioutil" |
104 | - "launchpad.net/goyaml" |
105 | - "launchpad.net/juju-core/log" |
106 | - "launchpad.net/juju-core/store" |
107 | - "launchpad.net/lpad" |
108 | - stdlog "log" |
109 | - "os" |
110 | - "path/filepath" |
111 | -) |
112 | - |
113 | -func main() { |
114 | - log.Target = stdlog.New(os.Stdout, "", stdlog.LstdFlags) |
115 | - err := load() |
116 | - if err != nil { |
117 | - fmt.Fprintf(os.Stderr, "%v\n", err) |
118 | - os.Exit(1) |
119 | - } |
120 | -} |
121 | - |
122 | -type config struct { |
123 | - MongoURL string `yaml:"mongo-url"` |
124 | -} |
125 | - |
126 | -func readConfig(path string, conf interface{}) error { |
127 | - f, err := os.Open(path) |
128 | - if err != nil { |
129 | - return fmt.Errorf("opening config file: %v", err) |
130 | - } |
131 | - data, err := ioutil.ReadAll(f) |
132 | - f.Close() |
133 | - if err != nil { |
134 | - return fmt.Errorf("reading config file: %v", err) |
135 | - } |
136 | - err = goyaml.Unmarshal(data, conf) |
137 | - if err != nil { |
138 | - return fmt.Errorf("processing config file: %v", err) |
139 | - } |
140 | - return nil |
141 | -} |
142 | - |
143 | -func load() error { |
144 | - var confPath string |
145 | - if len(os.Args) == 2 { |
146 | - if _, err := os.Stat(os.Args[1]); err == nil { |
147 | - confPath = os.Args[1] |
148 | - } |
149 | - } |
150 | - if confPath == "" { |
151 | - return fmt.Errorf("usage: %s <config path>", filepath.Base(os.Args[0])) |
152 | - } |
153 | - var conf config |
154 | - err := readConfig(confPath, &conf) |
155 | - if err != nil { |
156 | - return err |
157 | - } |
158 | - if conf.MongoURL == "" { |
159 | - return fmt.Errorf("missing mongo-url in config file") |
160 | - } |
161 | - s, err := store.Open(conf.MongoURL) |
162 | - if err != nil { |
163 | - return err |
164 | - } |
165 | - defer s.Close() |
166 | - err = store.PublishCharmsDistro(s, lpad.Production) |
167 | - if _, ok := err.(store.PublishBranchErrors); ok { |
168 | - // Ignore branch errors since they're commonplace here. |
169 | - // They're logged, though. |
170 | - return nil |
171 | - } |
172 | - return err |
173 | -} |
174 | |
175 | === removed directory 'store' |
176 | === removed file 'store/branch.go' |
177 | --- store/branch.go 2012-07-24 09:36:51 +0000 |
178 | +++ store/branch.go 1970-01-01 00:00:00 +0000 |
179 | @@ -1,152 +0,0 @@ |
180 | -package store |
181 | - |
182 | -import ( |
183 | - "bytes" |
184 | - "fmt" |
185 | - "io/ioutil" |
186 | - "launchpad.net/juju-core/charm" |
187 | - "os" |
188 | - "os/exec" |
189 | - "path/filepath" |
190 | - "strings" |
191 | -) |
192 | - |
193 | -// PublishBazaarBranch checks out the Bazaar branch from burl and |
194 | -// publishes its latest revision at urls in the given store. |
195 | -// The digest parameter must be the most recent known Bazaar |
196 | -// revision id for the branch tip. If publishing this specific digest |
197 | -// for these URLs has been attempted already, the publishing |
198 | -// procedure may abort early. The published digest is the Bazaar |
199 | -// revision id of the checked out branch's tip, though, which may |
200 | -// differ from the digest parameter. |
201 | -func PublishBazaarBranch(store *Store, urls []*charm.URL, burl string, digest string) error { |
202 | - |
203 | - // Prevent other publishers from updating these specific URLs |
204 | - // concurrently. |
205 | - lock, err := store.LockUpdates(urls) |
206 | - if err != nil { |
207 | - return err |
208 | - } |
209 | - defer lock.Unlock() |
210 | - |
211 | - var branchDir string |
212 | -NewTip: |
213 | - // Prepare the charm publisher. This will compute the revision |
214 | - // to be assigned to the charm, and it will also fail if the |
215 | - // operation is unnecessary because charms are up-to-date. |
216 | - pub, err := store.CharmPublisher(urls, digest) |
217 | - if err != nil { |
218 | - return err |
219 | - } |
220 | - |
221 | - // Figure if publishing this charm was already attempted before and |
222 | - // failed. We won't try again endlessly if so. In the future we may |
223 | - // retry automatically in certain circumstances. |
224 | - event, err := store.CharmEvent(urls[0], digest) |
225 | - if err == nil && event.Kind != EventPublished { |
226 | - return fmt.Errorf("charm publishing previously failed: %s", strings.Join(event.Errors, "; ")) |
227 | - } else if err != nil && err != ErrNotFound { |
228 | - return err |
229 | - } |
230 | - |
231 | - if branchDir == "" { |
232 | - // Retrieve the branch with a lightweight checkout, so that it |
233 | - // builds a working tree as cheaply as possible. History |
234 | - // doesn't matter here. |
235 | - tempDir, err := ioutil.TempDir("", "publish-branch-") |
236 | - if err != nil { |
237 | - return err |
238 | - } |
239 | - defer os.RemoveAll(tempDir) |
240 | - branchDir = filepath.Join(tempDir, "branch") |
241 | - output, err := exec.Command("bzr", "checkout", "--lightweight", burl, branchDir).CombinedOutput() |
242 | - if err != nil { |
243 | - return outputErr(output, err) |
244 | - } |
245 | - |
246 | - // Pick actual digest from tip. Publishing the real tip |
247 | - // revision rather than the revision for the digest provided is |
248 | - // strictly necessary to prevent a race condition. If the |
249 | - // provided digest was published instead, there's a chance |
250 | - // another publisher concurrently running could have found a |
251 | - // newer revision and published that first, and the digest |
252 | - // parameter provided is in fact an old version that would |
253 | - // overwrite the new version. |
254 | - tipDigest, err := bzrRevisionId(branchDir) |
255 | - if err != nil { |
256 | - return err |
257 | - } |
258 | - if tipDigest != digest { |
259 | - digest = tipDigest |
260 | - goto NewTip |
261 | - } |
262 | - } |
263 | - |
264 | - ch, err := charm.ReadDir(branchDir) |
265 | - if err == nil { |
266 | - // Hand over the charm to the store for bundling and |
267 | - // streaming its content into the database. |
268 | - err = pub.Publish(ch) |
269 | - if err == ErrUpdateConflict { |
270 | - // A conflict may happen in edge cases if the whole |
271 | - // locking mechanism fails due to an expiration event, |
272 | - // and then the expired concurrent publisher revives |
273 | - // for whatever reason and attempts to finish |
274 | - // publishing. The state of the system is still |
275 | - // consistent in that case, and the error isn't logged |
276 | - // since the revision was properly published before. |
277 | - return err |
278 | - } |
279 | - } |
280 | - |
281 | - // Publishing is done. Log failure or error. |
282 | - event = &CharmEvent{ |
283 | - URLs: urls, |
284 | - Digest: digest, |
285 | - } |
286 | - if err == nil { |
287 | - event.Kind = EventPublished |
288 | - event.Revision = pub.Revision() |
289 | - } else { |
290 | - event.Kind = EventPublishError |
291 | - event.Errors = []string{err.Error()} |
292 | - } |
293 | - if logerr := store.LogCharmEvent(event); logerr != nil { |
294 | - if err == nil { |
295 | - err = logerr |
296 | - } else { |
297 | - err = fmt.Errorf("%v; %v", err, logerr) |
298 | - } |
299 | - } |
300 | - return err |
301 | -} |
302 | - |
303 | -// bzrRevisionId returns the Bazaar revision id for the branch in branchDir. |
304 | -func bzrRevisionId(branchDir string) (string, error) { |
305 | - cmd := exec.Command("bzr", "revision-info") |
306 | - cmd.Dir = branchDir |
307 | - stderr := &bytes.Buffer{} |
308 | - cmd.Stderr = stderr |
309 | - output, err := cmd.Output() |
310 | - if err != nil { |
311 | - output = append(output, '\n') |
312 | - output = append(output, stderr.Bytes()...) |
313 | - return "", outputErr(output, err) |
314 | - } |
315 | - pair := bytes.Fields(output) |
316 | - if len(pair) != 2 { |
317 | - output = append(output, '\n') |
318 | - output = append(output, stderr.Bytes()...) |
319 | - return "", fmt.Errorf(`invalid output from "bzr revision-info": %s`, output) |
320 | - } |
321 | - return string(pair[1]), nil |
322 | -} |
323 | - |
324 | -// outputErr returns an error that assembles some command's output and its |
325 | -// error, if both output and err are set, and returns only err if output is nil. |
326 | -func outputErr(output []byte, err error) error { |
327 | - if len(output) > 0 { |
328 | - return fmt.Errorf("%v\n%s", err, output) |
329 | - } |
330 | - return err |
331 | -} |
332 | |
333 | === removed file 'store/branch_test.go' |
334 | --- store/branch_test.go 2012-10-09 01:19:01 +0000 |
335 | +++ store/branch_test.go 1970-01-01 00:00:00 +0000 |
336 | @@ -1,238 +0,0 @@ |
337 | -package store_test |
338 | - |
339 | -import ( |
340 | - "bytes" |
341 | - "fmt" |
342 | - "io/ioutil" |
343 | - . "launchpad.net/gocheck" |
344 | - "launchpad.net/juju-core/charm" |
345 | - "launchpad.net/juju-core/store" |
346 | - "launchpad.net/juju-core/testing" |
347 | - "os" |
348 | - "os/exec" |
349 | - "path/filepath" |
350 | - "strings" |
351 | - "time" |
352 | -) |
353 | - |
354 | -func (s *StoreSuite) dummyBranch(c *C, suffix string) bzrDir { |
355 | - tmpDir := c.MkDir() |
356 | - if suffix != "" { |
357 | - tmpDir = filepath.Join(tmpDir, suffix) |
358 | - err := os.MkdirAll(tmpDir, 0755) |
359 | - c.Assert(err, IsNil) |
360 | - } |
361 | - branch := bzrDir(tmpDir) |
362 | - branch.init() |
363 | - |
364 | - copyCharmDir(branch.path(), testing.Charms.Dir("series", "dummy")) |
365 | - branch.add() |
366 | - branch.commit("Imported charm.") |
367 | - return branch |
368 | -} |
369 | - |
370 | -var urls = []*charm.URL{ |
371 | - charm.MustParseURL("cs:~joe/oneiric/dummy"), |
372 | - charm.MustParseURL("cs:oneiric/dummy"), |
373 | -} |
374 | - |
375 | -type fakePlugin struct { |
376 | - oldEnv string |
377 | -} |
378 | - |
379 | -func (p *fakePlugin) install(dir string, content string) { |
380 | - p.oldEnv = os.Getenv("BZR_PLUGINS_AT") |
381 | - err := ioutil.WriteFile(filepath.Join(dir, "__init__.py"), []byte(content), 0644) |
382 | - if err != nil { |
383 | - panic(err) |
384 | - } |
385 | - os.Setenv("BZR_PLUGINS_AT", "fakePlugin@"+dir) |
386 | -} |
387 | - |
388 | -func (p *fakePlugin) uninstall() { |
389 | - os.Setenv("BZR_PLUGINS_AT", p.oldEnv) |
390 | -} |
391 | - |
392 | -func (s *StoreSuite) TestPublish(c *C) { |
393 | - branch := s.dummyBranch(c, "") |
394 | - |
395 | - // Ensure that the streams are parsed separately by inserting |
396 | - // garbage on stderr. The wanted information is still there. |
397 | - plugin := fakePlugin{} |
398 | - plugin.install(c.MkDir(), `import sys; sys.stderr.write("STDERR STUFF FROM TEST\n")`) |
399 | - defer plugin.uninstall() |
400 | - |
401 | - err := store.PublishBazaarBranch(s.store, urls, branch.path(), "wrong-rev") |
402 | - c.Assert(err, IsNil) |
403 | - |
404 | - for _, url := range urls { |
405 | - info, rc, err := s.store.OpenCharm(url) |
406 | - c.Assert(err, IsNil) |
407 | - defer rc.Close() |
408 | - c.Assert(info.Revision(), Equals, 0) |
409 | - c.Assert(info.Meta().Name, Equals, "dummy") |
410 | - |
411 | - data, err := ioutil.ReadAll(rc) |
412 | - c.Assert(err, IsNil) |
413 | - |
414 | - bundle, err := charm.ReadBundleBytes(data) |
415 | - c.Assert(err, IsNil) |
416 | - c.Assert(bundle.Revision(), Equals, 0) |
417 | - c.Assert(bundle.Meta().Name, Equals, "dummy") |
418 | - } |
419 | - |
420 | - // Attempt to publish the same content again while providing the wrong |
421 | - // tip revision. It must pick the real revision from the branch and |
422 | - // note this was previously published. |
423 | - err = store.PublishBazaarBranch(s.store, urls, branch.path(), "wrong-rev") |
424 | - c.Assert(err, Equals, store.ErrRedundantUpdate) |
425 | - |
426 | - // Bump the content revision and lie again about the known tip revision. |
427 | - // This time, though, pretend it's the same as the real branch revision |
428 | - // previously published. It must error and not publish the new revision |
429 | - // because it will use the revision provided as a parameter to check if |
430 | - // publishing was attempted before. This is the mechanism that enables |
431 | - // stopping fast without having to download every single branch. Real |
432 | - // revision is picked in the next scan. |
433 | - digest1 := branch.digest() |
434 | - branch.change() |
435 | - err = store.PublishBazaarBranch(s.store, urls, branch.path(), digest1) |
436 | - c.Assert(err, Equals, store.ErrRedundantUpdate) |
437 | - |
438 | - // Now allow it to publish the new content by providing an unseen revision. |
439 | - err = store.PublishBazaarBranch(s.store, urls, branch.path(), "wrong-rev") |
440 | - c.Assert(err, IsNil) |
441 | - digest2 := branch.digest() |
442 | - |
443 | - info, err := s.store.CharmInfo(urls[0]) |
444 | - c.Assert(err, IsNil) |
445 | - c.Assert(info.Revision(), Equals, 1) |
446 | - c.Assert(info.Meta().Name, Equals, "dummy") |
447 | - |
448 | - // There are two events published, for each of the successful attempts. |
449 | - // The failures are ignored given that they are artifacts of the |
450 | - // publishing mechanism rather than actual problems. |
451 | - _, err = s.store.CharmEvent(urls[0], "wrong-rev") |
452 | - c.Assert(err, Equals, store.ErrNotFound) |
453 | - for i, digest := range []string{digest1, digest2} { |
454 | - event, err := s.store.CharmEvent(urls[0], digest) |
455 | - c.Assert(err, IsNil) |
456 | - c.Assert(event.Kind, Equals, store.EventPublished) |
457 | - c.Assert(event.Revision, Equals, i) |
458 | - c.Assert(event.Errors, IsNil) |
459 | - c.Assert(event.Warnings, IsNil) |
460 | - } |
461 | -} |
462 | - |
463 | -func (s *StoreSuite) TestPublishErrorFromBzr(c *C) { |
464 | - branch := s.dummyBranch(c, "") |
465 | - |
466 | - // In TestPublish we ensure that the streams are parsed |
467 | - // separately by inserting garbage on stderr. Now make |
468 | - // sure that stderr isn't simply trashed, as we want to |
469 | - // know about what a real error tells us. |
470 | - plugin := fakePlugin{} |
471 | - plugin.install(c.MkDir(), `import sys; sys.stderr.write("STDERR STUFF FROM TEST\n"); sys.exit(1)`) |
472 | - defer plugin.uninstall() |
473 | - |
474 | - err := store.PublishBazaarBranch(s.store, urls, branch.path(), "wrong-rev") |
475 | - c.Assert(err, ErrorMatches, "(?s).*STDERR STUFF.*") |
476 | -} |
477 | - |
478 | -func (s *StoreSuite) TestPublishErrorInCharm(c *C) { |
479 | - branch := s.dummyBranch(c, "") |
480 | - |
481 | - // Corrupt the charm. |
482 | - branch.remove("metadata.yaml") |
483 | - branch.commit("Removed metadata.yaml.") |
484 | - |
485 | - // Attempt to publish the erroneous content. |
486 | - err := store.PublishBazaarBranch(s.store, urls, branch.path(), "wrong-rev") |
487 | - c.Assert(err, ErrorMatches, ".*/metadata.yaml: no such file or directory") |
488 | - |
489 | - // The event should be logged as well, since this was an error in the charm |
490 | - // that won't go away and must be communicated to the author. |
491 | - event, err := s.store.CharmEvent(urls[0], branch.digest()) |
492 | - c.Assert(err, IsNil) |
493 | - c.Assert(event.Kind, Equals, store.EventPublishError) |
494 | - c.Assert(event.Revision, Equals, 0) |
495 | - c.Assert(event.Errors, NotNil) |
496 | - c.Assert(event.Errors[0], Matches, ".*/metadata.yaml: no such file or directory") |
497 | - c.Assert(event.Warnings, IsNil) |
498 | -} |
499 | - |
500 | -type bzrDir string |
501 | - |
502 | -func (dir bzrDir) path(args ...string) string { |
503 | - return filepath.Join(append([]string{string(dir)}, args...)...) |
504 | -} |
505 | - |
506 | -func (dir bzrDir) run(args ...string) []byte { |
507 | - cmd := exec.Command("bzr", args...) |
508 | - oldemail := os.Getenv("EMAIL") |
509 | - defer os.Setenv("EMAIL", oldemail) |
510 | - // bzr will complain if bzr whoami has not been run previously, |
511 | - // avoid this by passing $EMAIL into the environment. |
512 | - os.Setenv("EMAIL", "nobody@example.com") |
513 | - cmd.Dir = string(dir) |
514 | - output, err := cmd.Output() |
515 | - if err != nil { |
516 | - panic(fmt.Sprintf("command failed: bzr %s\n%s", strings.Join(args, " "), output)) |
517 | - } |
518 | - return output |
519 | -} |
520 | - |
521 | -func (dir bzrDir) init() { |
522 | - dir.run("init") |
523 | -} |
524 | - |
525 | -func (dir bzrDir) add(paths ...string) { |
526 | - dir.run(append([]string{"add"}, paths...)...) |
527 | -} |
528 | - |
529 | -func (dir bzrDir) remove(paths ...string) { |
530 | - dir.run(append([]string{"rm"}, paths...)...) |
531 | -} |
532 | - |
533 | -func (dir bzrDir) commit(msg string) { |
534 | - dir.run("commit", "-m", msg) |
535 | -} |
536 | - |
537 | -func (dir bzrDir) write(path string, data string) { |
538 | - err := ioutil.WriteFile(dir.path(path), []byte(data), 0644) |
539 | - if err != nil { |
540 | - panic(err) |
541 | - } |
542 | -} |
543 | - |
544 | -func (dir bzrDir) change() { |
545 | - t := time.Now().String() |
546 | - dir.write("timestamp", t) |
547 | - dir.add("timestamp") |
548 | - dir.commit("Revision bumped at " + t) |
549 | -} |
550 | - |
551 | -func (dir bzrDir) digest() string { |
552 | - output := dir.run("revision-info") |
553 | - f := bytes.Fields(output) |
554 | - if len(f) != 2 { |
555 | - panic("revision-info returned bad output: " + string(output)) |
556 | - } |
557 | - return string(f[1]) |
558 | -} |
559 | - |
560 | -func copyCharmDir(dst string, dir *charm.Dir) { |
561 | - var b bytes.Buffer |
562 | - err := dir.BundleTo(&b) |
563 | - if err != nil { |
564 | - panic(err) |
565 | - } |
566 | - bundle, err := charm.ReadBundleBytes(b.Bytes()) |
567 | - if err != nil { |
568 | - panic(err) |
569 | - } |
570 | - err = bundle.ExpandTo(dst) |
571 | - if err != nil { |
572 | - panic(err) |
573 | - } |
574 | -} |
575 | |
576 | === removed file 'store/lpad.go' |
577 | --- store/lpad.go 2012-06-21 20:40:39 +0000 |
578 | +++ store/lpad.go 1970-01-01 00:00:00 +0000 |
579 | @@ -1,113 +0,0 @@ |
580 | -package store |
581 | - |
582 | -import ( |
583 | - "fmt" |
584 | - "launchpad.net/juju-core/charm" |
585 | - "launchpad.net/juju-core/log" |
586 | - "launchpad.net/lpad" |
587 | - "strings" |
588 | - "time" |
589 | -) |
590 | - |
591 | -type PublishBranchError struct { |
592 | - URL string |
593 | - Err error |
594 | -} |
595 | - |
596 | -type PublishBranchErrors []PublishBranchError |
597 | - |
598 | -func (errs PublishBranchErrors) Error() string { |
599 | - return fmt.Sprintf("%d branch(es) failed to be published", len(errs)) |
600 | -} |
601 | - |
602 | -// PublishCharmsDistro publishes all branch tips found in |
603 | -// the /charms distribution in Launchpad onto store under |
604 | -// the "cs:" scheme. |
605 | -// apiBase specifies the Launchpad base API URL, such |
606 | -// as lpad.Production or lpad.Staging. |
607 | -// Errors found while processing one or more branches are |
608 | -// all returned as a PublishBranchErrors value. |
609 | -func PublishCharmsDistro(store *Store, apiBase lpad.APIBase) error { |
610 | - oauth := &lpad.OAuth{Anonymous: true, Consumer: "juju"} |
611 | - root, err := lpad.Login(apiBase, oauth) |
612 | - if err != nil { |
613 | - return err |
614 | - } |
615 | - distro, err := root.Distro("charms") |
616 | - if err != nil { |
617 | - return err |
618 | - } |
619 | - tips, err := distro.BranchTips(time.Time{}) |
620 | - if err != nil { |
621 | - return err |
622 | - } |
623 | - |
624 | - var errs PublishBranchErrors |
625 | - for _, tip := range tips { |
626 | - if !strings.HasSuffix(tip.UniqueName, "/trunk") { |
627 | - continue |
628 | - } |
629 | - burl, curl, err := uniqueNameURLs(tip.UniqueName) |
630 | - if err != nil { |
631 | - errs = append(errs, PublishBranchError{tip.UniqueName, err}) |
632 | - log.Printf("error: %v\n", err) |
633 | - continue |
634 | - } |
635 | - log.Printf("----- %s\n", burl) |
636 | - if tip.Revision == "" { |
637 | - errs = append(errs, PublishBranchError{burl, fmt.Errorf("branch has no revisions")}) |
638 | - log.Printf("error: branch has no revisions\n") |
639 | - continue |
640 | - } |
641 | - // Charm is published in the personal URL and in any explicitly |
642 | - // assigned official series. |
643 | - urls := []*charm.URL{curl} |
644 | - schema, name := curl.Schema, curl.Name |
645 | - for _, series := range tip.OfficialSeries { |
646 | - curl = &charm.URL{Schema: schema, Name: name, Series: series, Revision: -1} |
647 | - curl.Series = series |
648 | - curl.User = "" |
649 | - urls = append(urls, curl) |
650 | - } |
651 | - |
652 | - err = PublishBazaarBranch(store, urls, burl, tip.Revision) |
653 | - if err == ErrRedundantUpdate { |
654 | - continue |
655 | - } |
656 | - if err != nil { |
657 | - errs = append(errs, PublishBranchError{burl, err}) |
658 | - log.Printf("error: %v\n", err) |
659 | - } |
660 | - } |
661 | - if errs != nil { |
662 | - return errs |
663 | - } |
664 | - return nil |
665 | -} |
666 | - |
667 | -// uniqueNameURLs returns the branch URL and the charm URL for the |
668 | -// provided Launchpad branch unique name. The unique name must be |
669 | -// in the form: |
670 | -// |
671 | -// ~<user>/charms/<series>/<charm name>/trunk |
672 | -// |
673 | -// For testing purposes, if name has a prefix preceding a string in |
674 | -// this format, the prefix is stripped out for computing the charm |
675 | -// URL, and the unique name is returned unchanged as the branch URL. |
676 | -func uniqueNameURLs(name string) (burl string, curl *charm.URL, err error) { |
677 | - u := strings.Split(name, "/") |
678 | - if len(u) > 5 { |
679 | - u = u[len(u)-5:] |
680 | - burl = name |
681 | - } else { |
682 | - burl = "lp:" + name |
683 | - } |
684 | - if len(u) < 5 || u[1] != "charms" || u[4] != "trunk" || len(u[0]) == 0 || u[0][0] != '~' { |
685 | - return "", nil, fmt.Errorf("unwanted branch name: %s", name) |
686 | - } |
687 | - curl, err = charm.ParseURL(fmt.Sprintf("cs:%s/%s/%s", u[0], u[2], u[3])) |
688 | - if err != nil { |
689 | - return "", nil, err |
690 | - } |
691 | - return burl, curl, nil |
692 | -} |
693 | |
694 | === removed file 'store/lpad_test.go' |
695 | --- store/lpad_test.go 2012-08-18 22:48:02 +0000 |
696 | +++ store/lpad_test.go 1970-01-01 00:00:00 +0000 |
697 | @@ -1,68 +0,0 @@ |
698 | -package store_test |
699 | - |
700 | -import ( |
701 | - "fmt" |
702 | - . "launchpad.net/gocheck" |
703 | - "launchpad.net/juju-core/charm" |
704 | - "launchpad.net/juju-core/store" |
705 | - "launchpad.net/juju-core/testing" |
706 | - "launchpad.net/lpad" |
707 | -) |
708 | - |
709 | -var jsonType = map[string]string{ |
710 | - "Content-Type": "application/json", |
711 | -} |
712 | - |
713 | -func (s *StoreSuite) TestPublishCharmDistro(c *C) { |
714 | - branch := s.dummyBranch(c, "~joe/charms/oneiric/dummy/trunk") |
715 | - |
716 | - // The Distro call will look for bare /charms, first. |
717 | - testing.Server.Response(200, jsonType, []byte("{}")) |
718 | - |
719 | - // And then it picks up the tips. |
720 | - data := fmt.Sprintf(`[`+ |
721 | - `["file://%s", "rev1", ["oneiric", "precise"]],`+ |
722 | - `["file://%s", "%s", []],`+ |
723 | - `["file:///non-existent/~jeff/charms/precise/bad/trunk", "rev2", []],`+ |
724 | - `["file:///non-existent/~jeff/charms/precise/bad/skip-me", "rev3", []]`+ |
725 | - `]`, |
726 | - branch.path(), branch.path(), branch.digest()) |
727 | - testing.Server.Response(200, jsonType, []byte(data)) |
728 | - |
729 | - apiBase := lpad.APIBase(testing.Server.URL) |
730 | - err := store.PublishCharmsDistro(s.store, apiBase) |
731 | - |
732 | - // Should have a single failure from the trunk branch that doesn't |
733 | - // exist. The redundant update with the known digest should be |
734 | - // ignored, and skip-me isn't a supported branch name so it's |
735 | - // ignored as well. |
736 | - c.Assert(err, ErrorMatches, `1 branch\(es\) failed to be published`) |
737 | - berr := err.(store.PublishBranchErrors)[0] |
738 | - c.Assert(berr.URL, Equals, "file:///non-existent/~jeff/charms/precise/bad/trunk") |
739 | - c.Assert(berr.Err, ErrorMatches, "(?s).*bzr: ERROR: Not a branch.*") |
740 | - |
741 | - for _, url := range []string{"cs:oneiric/dummy", "cs:precise/dummy-0", "cs:~joe/oneiric/dummy-0"} { |
742 | - dummy, err := s.store.CharmInfo(charm.MustParseURL(url)) |
743 | - c.Assert(err, IsNil) |
744 | - c.Assert(dummy.Meta().Name, Equals, "dummy") |
745 | - } |
746 | - |
747 | - // The known digest should have been ignored, so revision is still at 0. |
748 | - _, err = s.store.CharmInfo(charm.MustParseURL("cs:~joe/oneiric/dummy-1")) |
749 | - c.Assert(err, Equals, store.ErrNotFound) |
750 | - |
751 | - // bare /charms lookup |
752 | - req := testing.Server.WaitRequest() |
753 | - c.Assert(req.Method, Equals, "GET") |
754 | - c.Assert(req.URL.Path, Equals, "/charms") |
755 | - |
756 | - // tips request |
757 | - req = testing.Server.WaitRequest() |
758 | - c.Assert(req.Method, Equals, "GET") |
759 | - c.Assert(req.URL.Path, Equals, "/charms") |
760 | - c.Assert(req.Form["ws.op"], DeepEquals, []string{"getBranchTips"}) |
761 | - c.Assert(req.Form["since"], IsNil) |
762 | - |
763 | - // Request must be signed by juju. |
764 | - c.Assert(req.Header.Get("Authorization"), Matches, `.*oauth_consumer_key="juju".*`) |
765 | -} |
766 | |
767 | === removed file 'store/mgo_test.go' |
768 | --- store/mgo_test.go 2012-10-30 11:45:32 +0000 |
769 | +++ store/mgo_test.go 1970-01-01 00:00:00 +0000 |
770 | @@ -1,95 +0,0 @@ |
771 | -package store_test |
772 | - |
773 | -import ( |
774 | - "bytes" |
775 | - "labix.org/v2/mgo" |
776 | - . "launchpad.net/gocheck" |
777 | - "os/exec" |
778 | - "time" |
779 | -) |
780 | - |
781 | -// ---------------------------------------------------------------------------- |
782 | -// The mgo test suite |
783 | - |
784 | -type MgoSuite struct { |
785 | - Addr string |
786 | - Session *mgo.Session |
787 | - output bytes.Buffer |
788 | - server *exec.Cmd |
789 | -} |
790 | - |
791 | -func (s *MgoSuite) SetUpSuite(c *C) { |
792 | - mgo.SetDebug(true) |
793 | - mgo.SetStats(true) |
794 | - dbdir := c.MkDir() |
795 | - args := []string{ |
796 | - "--dbpath", dbdir, |
797 | - "--bind_ip", "127.0.0.1", |
798 | - "--port", "50017", |
799 | - "--nssize", "1", |
800 | - "--noprealloc", |
801 | - "--smallfiles", |
802 | - "--nojournal", |
803 | - } |
804 | - s.server = exec.Command("mongod", args...) |
805 | - s.server.Stdout = &s.output |
806 | - s.server.Stderr = &s.output |
807 | - err := s.server.Start() |
808 | - c.Assert(err, IsNil) |
809 | -} |
810 | - |
811 | -func (s *MgoSuite) TearDownSuite(c *C) { |
812 | - s.server.Process.Kill() |
813 | - s.server.Process.Wait() |
814 | -} |
815 | - |
816 | -func (s *MgoSuite) SetUpTest(c *C) { |
817 | - err := DropAll("localhost:50017") |
818 | - c.Assert(err, IsNil) |
819 | - mgo.SetLogger(c) |
820 | - mgo.ResetStats() |
821 | - s.Addr = "127.0.0.1:50017" |
822 | - s.Session, err = mgo.Dial(s.Addr) |
823 | - c.Assert(err, IsNil) |
824 | -} |
825 | - |
826 | -func (s *MgoSuite) TearDownTest(c *C) { |
827 | - if s.Session != nil { |
828 | - s.Session.Close() |
829 | - } |
830 | - for i := 0; ; i++ { |
831 | - stats := mgo.GetStats() |
832 | - if stats.SocketsInUse == 0 && stats.SocketsAlive == 0 { |
833 | - break |
834 | - } |
835 | - if i == 20 { |
836 | - c.Fatal("Test left sockets in a dirty state") |
837 | - } |
838 | - c.Logf("Waiting for sockets to die: %d in use, %d alive", stats.SocketsInUse, stats.SocketsAlive) |
839 | - time.Sleep(500 * time.Millisecond) |
840 | - } |
841 | -} |
842 | - |
843 | -func DropAll(mongourl string) (err error) { |
844 | - session, err := mgo.Dial(mongourl) |
845 | - if err != nil { |
846 | - return err |
847 | - } |
848 | - defer session.Close() |
849 | - |
850 | - names, err := session.DatabaseNames() |
851 | - if err != nil { |
852 | - return err |
853 | - } |
854 | - for _, name := range names { |
855 | - switch name { |
856 | - case "admin", "local", "config": |
857 | - default: |
858 | - err = session.DB(name).DropDatabase() |
859 | - if err != nil { |
860 | - return err |
861 | - } |
862 | - } |
863 | - } |
864 | - return nil |
865 | -} |
866 | |
867 | === removed file 'store/server.go' |
868 | --- store/server.go 2012-10-11 14:52:21 +0000 |
869 | +++ store/server.go 1970-01-01 00:00:00 +0000 |
870 | @@ -1,191 +0,0 @@ |
871 | -package store |
872 | - |
873 | -import ( |
874 | - "encoding/json" |
875 | - "io" |
876 | - "launchpad.net/juju-core/charm" |
877 | - "launchpad.net/juju-core/log" |
878 | - "net/http" |
879 | - "strconv" |
880 | - "strings" |
881 | -) |
882 | - |
883 | -// Server is an http.Handler that serves the HTTP API of juju |
884 | -// so that juju clients can retrieve published charms. |
885 | -type Server struct { |
886 | - store *Store |
887 | - mux *http.ServeMux |
888 | -} |
889 | - |
890 | -// New returns a new *Server using store. |
891 | -func NewServer(store *Store) (*Server, error) { |
892 | - s := &Server{ |
893 | - store: store, |
894 | - mux: http.NewServeMux(), |
895 | - } |
896 | - s.mux.HandleFunc("/charm-info", func(w http.ResponseWriter, r *http.Request) { |
897 | - s.serveInfo(w, r) |
898 | - }) |
899 | - s.mux.HandleFunc("/charm/", func(w http.ResponseWriter, r *http.Request) { |
900 | - s.serveCharm(w, r) |
901 | - }) |
902 | - s.mux.HandleFunc("/stats/counter/", func(w http.ResponseWriter, r *http.Request) { |
903 | - s.serveStats(w, r) |
904 | - }) |
905 | - |
906 | - // This is just a validation key to allow blitz.io to run |
907 | - // performance tests against the site. |
908 | - s.mux.HandleFunc("/mu-35700a31-6bf320ca-a800b670-05f845ee", func(w http.ResponseWriter, r *http.Request) { |
909 | - s.serveBlitzKey(w, r) |
910 | - }) |
911 | - return s, nil |
912 | -} |
913 | - |
914 | -// ServeHTTP serves an http request. |
915 | -// This method turns *Server into an http.Handler. |
916 | -func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
917 | - if r.URL.Path == "/" { |
918 | - http.Redirect(w, r, "https://juju.ubuntu.com", http.StatusSeeOther) |
919 | - return |
920 | - } |
921 | - s.mux.ServeHTTP(w, r) |
922 | -} |
923 | - |
924 | -func statsEnabled(req *http.Request) bool { |
925 | - // It's fine to parse the form more than once, and it avoids |
926 | - // bugs from not parsing it. |
927 | - req.ParseForm() |
928 | - return req.Form.Get("stats") != "0" |
929 | -} |
930 | - |
931 | -func charmStatsKey(curl *charm.URL, kind string) []string { |
932 | - if curl.User == "" { |
933 | - return []string{kind, curl.Series, curl.Name} |
934 | - } |
935 | - return []string{kind, curl.Series, curl.Name, curl.User} |
936 | -} |
937 | - |
938 | -func (s *Server) serveInfo(w http.ResponseWriter, r *http.Request) { |
939 | - if r.URL.Path != "/charm-info" { |
940 | - w.WriteHeader(http.StatusNotFound) |
941 | - return |
942 | - } |
943 | - r.ParseForm() |
944 | - response := map[string]*charm.InfoResponse{} |
945 | - for _, url := range r.Form["charms"] { |
946 | - c := &charm.InfoResponse{} |
947 | - response[url] = c |
948 | - curl, err := charm.ParseURL(url) |
949 | - var info *CharmInfo |
950 | - if err == nil { |
951 | - info, err = s.store.CharmInfo(curl) |
952 | - } |
953 | - var skey []string |
954 | - if err == nil { |
955 | - skey = charmStatsKey(curl, "charm-info") |
956 | - c.Sha256 = info.BundleSha256() |
957 | - c.Revision = info.Revision() |
958 | - c.Digest = info.Digest() |
959 | - } else { |
960 | - if err == ErrNotFound { |
961 | - skey = charmStatsKey(curl, "charm-missing") |
962 | - } |
963 | - c.Errors = append(c.Errors, err.Error()) |
964 | - } |
965 | - if skey != nil && statsEnabled(r) { |
966 | - go s.store.IncCounter(skey) |
967 | - } |
968 | - } |
969 | - data, err := json.Marshal(response) |
970 | - if err == nil { |
971 | - w.Header().Set("Content-Type", "application/json") |
972 | - _, err = w.Write(data) |
973 | - } |
974 | - if err != nil { |
975 | - log.Printf("store: cannot write content: %v", err) |
976 | - w.WriteHeader(http.StatusInternalServerError) |
977 | - return |
978 | - } |
979 | -} |
980 | - |
981 | -func (s *Server) serveCharm(w http.ResponseWriter, r *http.Request) { |
982 | - if !strings.HasPrefix(r.URL.Path, "/charm/") { |
983 | - panic("serveCharm: bad url") |
984 | - } |
985 | - curl, err := charm.ParseURL("cs:" + r.URL.Path[len("/charm/"):]) |
986 | - if err != nil { |
987 | - w.WriteHeader(http.StatusNotFound) |
988 | - return |
989 | - } |
990 | - info, rc, err := s.store.OpenCharm(curl) |
991 | - if err == ErrNotFound { |
992 | - w.WriteHeader(http.StatusNotFound) |
993 | - return |
994 | - } |
995 | - if err != nil { |
996 | - w.WriteHeader(http.StatusInternalServerError) |
997 | - log.Printf("store: cannot open charm %q: %v", curl, err) |
998 | - return |
999 | - } |
1000 | - if statsEnabled(r) { |
1001 | - go s.store.IncCounter(charmStatsKey(curl, "charm-bundle")) |
1002 | - } |
1003 | - defer rc.Close() |
1004 | - w.Header().Set("Connection", "close") // No keep-alive for now. |
1005 | - w.Header().Set("Content-Type", "application/octet-stream") |
1006 | - w.Header().Set("Content-Length", strconv.FormatInt(info.BundleSize(), 10)) |
1007 | - _, err = io.Copy(w, rc) |
1008 | - if err != nil { |
1009 | - log.Printf("store: failed to stream charm %q: %v", curl, err) |
1010 | - } |
1011 | -} |
1012 | - |
1013 | -func (s *Server) serveStats(w http.ResponseWriter, r *http.Request) { |
1014 | - // TODO: Adopt a smarter mux that simplifies this logic. |
1015 | - const dir = "/stats/counter/" |
1016 | - if !strings.HasPrefix(r.URL.Path, dir) { |
1017 | - panic("bad url") |
1018 | - } |
1019 | - base := r.URL.Path[len(dir):] |
1020 | - if strings.Index(base, "/") > 0 { |
1021 | - w.WriteHeader(http.StatusNotFound) |
1022 | - return |
1023 | - } |
1024 | - if base == "" { |
1025 | - w.WriteHeader(http.StatusForbidden) |
1026 | - return |
1027 | - } |
1028 | - key := strings.Split(base, ":") |
1029 | - prefix := false |
1030 | - if key[len(key)-1] == "*" { |
1031 | - prefix = true |
1032 | - key = key[:len(key)-1] |
1033 | - if len(key) == 0 { |
1034 | - // No point in counting something unknown. |
1035 | - w.WriteHeader(http.StatusForbidden) |
1036 | - return |
1037 | - } |
1038 | - } |
1039 | - r.ParseForm() |
1040 | - sum, err := s.store.SumCounter(key, prefix) |
1041 | - if err != nil { |
1042 | - log.Printf("store: cannot sum counter: %v", err) |
1043 | - w.WriteHeader(http.StatusInternalServerError) |
1044 | - return |
1045 | - } |
1046 | - data := []byte(strconv.FormatInt(sum, 10)) |
1047 | - w.Header().Set("Content-Type", "text/plain") |
1048 | - w.Header().Set("Content-Length", strconv.Itoa(len(data))) |
1049 | - _, err = w.Write(data) |
1050 | - if err != nil { |
1051 | - log.Printf("store: cannot write content: %v", err) |
1052 | - w.WriteHeader(http.StatusInternalServerError) |
1053 | - } |
1054 | -} |
1055 | - |
1056 | -func (s *Server) serveBlitzKey(w http.ResponseWriter, r *http.Request) { |
1057 | - w.Header().Set("Connection", "close") |
1058 | - w.Header().Set("Content-Type", "text/plain") |
1059 | - w.Header().Set("Content-Length", "2") |
1060 | - w.Write([]byte("42")) |
1061 | -} |
1062 | |
1063 | === removed file 'store/server_test.go' |
1064 | --- store/server_test.go 2012-09-05 21:08:26 +0000 |
1065 | +++ store/server_test.go 1970-01-01 00:00:00 +0000 |
1066 | @@ -1,209 +0,0 @@ |
1067 | -package store_test |
1068 | - |
1069 | -import ( |
1070 | - "encoding/json" |
1071 | - "io/ioutil" |
1072 | - . "launchpad.net/gocheck" |
1073 | - "launchpad.net/juju-core/charm" |
1074 | - "launchpad.net/juju-core/store" |
1075 | - "net/http" |
1076 | - "net/http/httptest" |
1077 | - "net/url" |
1078 | - "strconv" |
1079 | - "time" |
1080 | -) |
1081 | - |
1082 | -func (s *StoreSuite) prepareServer(c *C) (*store.Server, *charm.URL) { |
1083 | - curl := charm.MustParseURL("cs:oneiric/wordpress") |
1084 | - pub, err := s.store.CharmPublisher([]*charm.URL{curl}, "some-digest") |
1085 | - c.Assert(err, IsNil) |
1086 | - err = pub.Publish(&FakeCharmDir{}) |
1087 | - c.Assert(err, IsNil) |
1088 | - |
1089 | - server, err := store.NewServer(s.store) |
1090 | - c.Assert(err, IsNil) |
1091 | - return server, curl |
1092 | -} |
1093 | - |
1094 | -func (s *StoreSuite) TestServerCharmInfo(c *C) { |
1095 | - server, curl := s.prepareServer(c) |
1096 | - req, err := http.NewRequest("GET", "/charm-info", nil) |
1097 | - c.Assert(err, IsNil) |
1098 | - |
1099 | - var tests = []struct{ url, sha, digest, err string }{ |
1100 | - {curl.String(), fakeRevZeroSha, "some-digest", ""}, |
1101 | - {"cs:oneiric/non-existent", "", "", "entry not found"}, |
1102 | - {"cs:bad", "", "", `charm URL without series: "cs:bad"`}, |
1103 | - } |
1104 | - |
1105 | - for _, t := range tests { |
1106 | - req.Form = url.Values{"charms": []string{t.url}} |
1107 | - rec := httptest.NewRecorder() |
1108 | - server.ServeHTTP(rec, req) |
1109 | - |
1110 | - expected := make(map[string]interface{}) |
1111 | - if t.sha != "" { |
1112 | - expected[t.url] = map[string]interface{}{ |
1113 | - "revision": float64(0), |
1114 | - "sha256": t.sha, |
1115 | - "digest": t.digest, |
1116 | - } |
1117 | - } else { |
1118 | - expected[t.url] = map[string]interface{}{ |
1119 | - "revision": float64(0), |
1120 | - "errors": []interface{}{t.err}, |
1121 | - } |
1122 | - } |
1123 | - obtained := map[string]interface{}{} |
1124 | - err = json.NewDecoder(rec.Body).Decode(&obtained) |
1125 | - c.Assert(err, IsNil) |
1126 | - c.Assert(obtained, DeepEquals, expected) |
1127 | - c.Assert(rec.Header().Get("Content-Type"), Equals, "application/json") |
1128 | - } |
1129 | - |
1130 | - s.checkCounterSum(c, []string{"charm-info", curl.Series, curl.Name}, false, 1) |
1131 | - s.checkCounterSum(c, []string{"charm-missing", "oneiric", "non-existent"}, false, 1) |
1132 | -} |
1133 | - |
1134 | -// checkCounterSum checks that statistics are properly collected. |
1135 | -// It retries a few times as they are generally collected in background. |
1136 | -func (s *StoreSuite) checkCounterSum(c *C, key []string, prefix bool, expected int64) { |
1137 | - var sum int64 |
1138 | - var err error |
1139 | - for retry := 0; retry < 10; retry++ { |
1140 | - time.Sleep(1e8) |
1141 | - sum, err = s.store.SumCounter(key, prefix) |
1142 | - c.Assert(err, IsNil) |
1143 | - if sum == expected { |
1144 | - if expected == 0 && retry < 2 { |
1145 | - continue // Wait a bit to make sure. |
1146 | - } |
1147 | - return |
1148 | - } |
1149 | - } |
1150 | - c.Errorf("counter sum for %#v is %d, want %d", key, sum, expected) |
1151 | -} |
1152 | - |
1153 | -func (s *StoreSuite) TestCharmStreaming(c *C) { |
1154 | - server, curl := s.prepareServer(c) |
1155 | - |
1156 | - req, err := http.NewRequest("GET", "/charm/"+curl.String()[3:], nil) |
1157 | - c.Assert(err, IsNil) |
1158 | - rec := httptest.NewRecorder() |
1159 | - server.ServeHTTP(rec, req) |
1160 | - |
1161 | - data, err := ioutil.ReadAll(rec.Body) |
1162 | - c.Assert(string(data), Equals, "charm-revision-0") |
1163 | - |
1164 | - c.Assert(rec.Header().Get("Connection"), Equals, "close") |
1165 | - c.Assert(rec.Header().Get("Content-Type"), Equals, "application/octet-stream") |
1166 | - c.Assert(rec.Header().Get("Content-Length"), Equals, "16") |
1167 | - |
1168 | - // Check that it was accounted for in statistics. |
1169 | - s.checkCounterSum(c, []string{"charm-bundle", curl.Series, curl.Name}, false, 1) |
1170 | -} |
1171 | - |
1172 | -func (s *StoreSuite) TestDisableStats(c *C) { |
1173 | - server, curl := s.prepareServer(c) |
1174 | - |
1175 | - req, err := http.NewRequest("GET", "/charm-info", nil) |
1176 | - c.Assert(err, IsNil) |
1177 | - req.Form = url.Values{"charms": []string{curl.String()}, "stats": []string{"0"}} |
1178 | - rec := httptest.NewRecorder() |
1179 | - server.ServeHTTP(rec, req) |
1180 | - c.Assert(rec.Code, Equals, 200) |
1181 | - |
1182 | - req, err = http.NewRequest("GET", "/charm/"+curl.String()[3:], nil) |
1183 | - c.Assert(err, IsNil) |
1184 | - req.Form = url.Values{"stats": []string{"0"}} |
1185 | - rec = httptest.NewRecorder() |
1186 | - server.ServeHTTP(rec, req) |
1187 | - c.Assert(rec.Code, Equals, 200) |
1188 | - |
1189 | - // No statistics should have been collected given the use of stats=0. |
1190 | - for _, prefix := range []string{"charm-info", "charm-bundle", "charm-missing"} { |
1191 | - s.checkCounterSum(c, []string{prefix}, true, 0) |
1192 | - } |
1193 | -} |
1194 | - |
1195 | -func (s *StoreSuite) TestServerStatus(c *C) { |
1196 | - server, err := store.NewServer(s.store) |
1197 | - c.Assert(err, IsNil) |
1198 | - tests := []struct { |
1199 | - path string |
1200 | - code int |
1201 | - }{ |
1202 | - {"/charm-info/any", 404}, |
1203 | - {"/charm/bad-url", 404}, |
1204 | - {"/charm/bad-series/wordpress", 404}, |
1205 | - {"/stats/counter/", 403}, |
1206 | - {"/stats/counter/*", 403}, |
1207 | - {"/stats/counter/any/", 404}, |
1208 | - {"/stats/", 404}, |
1209 | - {"/stats/any", 404}, |
1210 | - } |
1211 | - for _, test := range tests { |
1212 | - req, err := http.NewRequest("GET", test.path, nil) |
1213 | - c.Assert(err, IsNil) |
1214 | - rec := httptest.NewRecorder() |
1215 | - server.ServeHTTP(rec, req) |
1216 | - c.Assert(rec.Code, Equals, test.code, Commentf("Path: %s", test.path)) |
1217 | - } |
1218 | -} |
1219 | - |
1220 | -func (s *StoreSuite) TestRootRedirect(c *C) { |
1221 | - server, err := store.NewServer(s.store) |
1222 | - c.Assert(err, IsNil) |
1223 | - req, err := http.NewRequest("GET", "/", nil) |
1224 | - c.Assert(err, IsNil) |
1225 | - rec := httptest.NewRecorder() |
1226 | - server.ServeHTTP(rec, req) |
1227 | - c.Assert(rec.Code, Equals, 303) |
1228 | - c.Assert(rec.Header().Get("Location"), Equals, "https://juju.ubuntu.com") |
1229 | -} |
1230 | - |
1231 | -func (s *StoreSuite) TestStatsCounter(c *C) { |
1232 | - for _, key := range [][]string{{"a", "b"}, {"a", "b"}, {"a"}} { |
1233 | - err := s.store.IncCounter(key) |
1234 | - c.Assert(err, IsNil) |
1235 | - } |
1236 | - |
1237 | - server, _ := s.prepareServer(c) |
1238 | - |
1239 | - expected := map[string]string{ |
1240 | - "a:b": "2", |
1241 | - "a:*": "3", |
1242 | - "a": "1", |
1243 | - } |
1244 | - |
1245 | - for counter, n := range expected { |
1246 | - req, err := http.NewRequest("GET", "/stats/counter/"+counter, nil) |
1247 | - c.Assert(err, IsNil) |
1248 | - rec := httptest.NewRecorder() |
1249 | - server.ServeHTTP(rec, req) |
1250 | - |
1251 | - data, err := ioutil.ReadAll(rec.Body) |
1252 | - c.Assert(string(data), Equals, n) |
1253 | - |
1254 | - c.Assert(rec.Header().Get("Content-Type"), Equals, "text/plain") |
1255 | - c.Assert(rec.Header().Get("Content-Length"), Equals, strconv.Itoa(len(n))) |
1256 | - } |
1257 | -} |
1258 | - |
1259 | -func (s *StoreSuite) TestBlitzKey(c *C) { |
1260 | - server, _ := s.prepareServer(c) |
1261 | - |
1262 | - // This is just a validation key to allow blitz.io to run |
1263 | - // performance tests against the site. |
1264 | - req, err := http.NewRequest("GET", "/mu-35700a31-6bf320ca-a800b670-05f845ee", nil) |
1265 | - c.Assert(err, IsNil) |
1266 | - rec := httptest.NewRecorder() |
1267 | - server.ServeHTTP(rec, req) |
1268 | - |
1269 | - data, err := ioutil.ReadAll(rec.Body) |
1270 | - c.Assert(string(data), Equals, "42") |
1271 | - |
1272 | - c.Assert(rec.Header().Get("Connection"), Equals, "close") |
1273 | - c.Assert(rec.Header().Get("Content-Type"), Equals, "text/plain") |
1274 | - c.Assert(rec.Header().Get("Content-Length"), Equals, "2") |
1275 | -} |
1276 | |
1277 | === removed file 'store/store.go' |
1278 | --- store/store.go 2012-10-11 17:40:17 +0000 |
1279 | +++ store/store.go 1970-01-01 00:00:00 +0000 |
1280 | @@ -1,774 +0,0 @@ |
1281 | -// The store package is capable of storing and updating charms in a MongoDB |
1282 | -// database, as well as maintaining further information about them such as |
1283 | -// the VCS revision the charm was loaded from and the URLs for the charms. |
1284 | -package store |
1285 | - |
1286 | -import ( |
1287 | - "crypto/sha256" |
1288 | - "encoding/hex" |
1289 | - "errors" |
1290 | - "fmt" |
1291 | - "hash" |
1292 | - "io" |
1293 | - "labix.org/v2/mgo" |
1294 | - "labix.org/v2/mgo/bson" |
1295 | - "launchpad.net/juju-core/charm" |
1296 | - "launchpad.net/juju-core/log" |
1297 | - "sort" |
1298 | - "strconv" |
1299 | - "sync" |
1300 | - "time" |
1301 | -) |
1302 | - |
1303 | -// The following MongoDB collections are currently used: |
1304 | -// |
1305 | -// juju.events - Log of events relating to the lifecycle of charms |
1306 | -// juju.charms - Information about the stored charms |
1307 | -// juju.charmfs.* - GridFS with the charm files |
1308 | -// juju.locks - Has unique keys with url of updating charms |
1309 | -// juju.stat.counters - Counters for statistics |
1310 | -// juju.stat.tokens - Tokens used in statistics counter keys |
1311 | - |
1312 | -var ( |
1313 | - ErrUpdateConflict = errors.New("charm update in progress") |
1314 | - ErrRedundantUpdate = errors.New("charm is up-to-date") |
1315 | - ErrNotFound = errors.New("entry not found") |
1316 | -) |
1317 | - |
1318 | -const ( |
1319 | - UpdateTimeout = 600e9 |
1320 | -) |
1321 | - |
1322 | -// Store holds a connection to a charm store. |
1323 | -type Store struct { |
1324 | - session *storeSession |
1325 | - |
1326 | - // Cache for statistics key words (two generations). |
1327 | - cacheMu sync.RWMutex |
1328 | - statsTokenNew map[string]int |
1329 | - statsTokenOld map[string]int |
1330 | -} |
1331 | - |
1332 | -// Open creates a new session with the store. It connects to the MongoDB |
1333 | -// server at the given address (as expected by the Mongo function in the |
1334 | -// labix.org/v2/mgo package). |
1335 | -func Open(mongoAddr string) (store *Store, err error) { |
1336 | - log.Printf("store: Store opened. Connecting to: %s", mongoAddr) |
1337 | - store = &Store{} |
1338 | - session, err := mgo.Dial(mongoAddr) |
1339 | - if err != nil { |
1340 | - log.Printf("store: Error connecting to MongoDB: %v", err) |
1341 | - return nil, err |
1342 | - } |
1343 | - |
1344 | - store = &Store{session: &storeSession{session}} |
1345 | - |
1346 | - // Ignore error. It'll always fail after created. |
1347 | - // TODO Check the error once mgo hands it to us. |
1348 | - _ = store.session.DB("juju").Run(bson.D{{"create", "stat.counters"}, {"autoIndexId", false}}, nil) |
1349 | - |
1350 | - if err := store.ensureIndexes(); err != nil { |
1351 | - session.Close() |
1352 | - return nil, err |
1353 | - } |
1354 | - |
1355 | - // Put the used socket back in the pool. |
1356 | - session.Refresh() |
1357 | - return store, nil |
1358 | -} |
1359 | - |
1360 | -func (s *Store) ensureIndexes() error { |
1361 | - session := s.session |
1362 | - indexes := []struct { |
1363 | - c *mgo.Collection |
1364 | - i mgo.Index |
1365 | - }{{ |
1366 | - session.StatCounters(), |
1367 | - mgo.Index{Key: []string{"k", "t"}, Unique: true}, |
1368 | - }, { |
1369 | - session.StatTokens(), |
1370 | - mgo.Index{Key: []string{"t"}, Unique: true}, |
1371 | - }, { |
1372 | - session.Charms(), |
1373 | - mgo.Index{Key: []string{"urls", "revision"}, Unique: true}, |
1374 | - }, { |
1375 | - session.Events(), |
1376 | - mgo.Index{Key: []string{"urls", "digest"}}, |
1377 | - }} |
1378 | - for _, idx := range indexes { |
1379 | - err := idx.c.EnsureIndex(idx.i) |
1380 | - if err != nil { |
1381 | - log.Printf("store: Error ensuring stat.counters index: %v", err) |
1382 | - return err |
1383 | - } |
1384 | - } |
1385 | - return nil |
1386 | -} |
1387 | - |
1388 | -// Close terminates the connection with the store. |
1389 | -func (s *Store) Close() { |
1390 | - s.session.Close() |
1391 | -} |
1392 | - |
1393 | -// statsKey returns the compound statistics identifier that represents key. |
1394 | -// If write is true, the identifier will be created if necessary. |
1395 | -// Identifiers have a form similar to "ab:c:def:", where each section is a |
1396 | -// base-32 number that represents the respective word in key. This form |
1397 | -// allows efficiently indexing and searching for prefixes, while detaching |
1398 | -// the key content and size from the actual words used in key. |
1399 | -func (s *Store) statsKey(session *storeSession, key []string, write bool) (string, error) { |
1400 | - if len(key) == 0 { |
1401 | - return "", fmt.Errorf("store: empty statistics key") |
1402 | - } |
1403 | - tokens := session.StatTokens() |
1404 | - skey := make([]byte, 0, len(key)*4) |
1405 | - // Retry limit is mainly to prevent infinite recursion in edge cases, |
1406 | - // such as if the database is ever run in read-only mode. |
1407 | - // The logic below should deteministically stop in normal scenarios. |
1408 | - var err error |
1409 | - for i, retry := 0, 30; i < len(key) && retry > 0; retry-- { |
1410 | - err = nil |
1411 | - id, found := s.statsTokenId(key[i]) |
1412 | - if !found { |
1413 | - var t struct { |
1414 | - Id int "_id" |
1415 | - Token string "t" |
1416 | - } |
1417 | - err = tokens.Find(bson.D{{"t", key[i]}}).One(&t) |
1418 | - if err == mgo.ErrNotFound { |
1419 | - if !write { |
1420 | - return "", ErrNotFound |
1421 | - } |
1422 | - t.Id, err = tokens.Count() |
1423 | - if err != nil { |
1424 | - continue |
1425 | - } |
1426 | - t.Id++ |
1427 | - t.Token = key[i] |
1428 | - err = tokens.Insert(&t) |
1429 | - } |
1430 | - if err != nil { |
1431 | - continue |
1432 | - } |
1433 | - s.cacheStatsTokenId(t.Token, t.Id) |
1434 | - id = t.Id |
1435 | - } |
1436 | - skey = strconv.AppendInt(skey, int64(id), 32) |
1437 | - skey = append(skey, ':') |
1438 | - i++ |
1439 | - } |
1440 | - if err != nil { |
1441 | - return "", err |
1442 | - } |
1443 | - return string(skey), nil |
1444 | -} |
1445 | - |
1446 | -const statsTokenCacheSize = 512 |
1447 | - |
1448 | -// cacheStatsTokenId adds the id for token into the cache. |
1449 | -// The cache has two generations so that the least frequently used |
1450 | -// tokens are evicted regularly. |
1451 | -func (s *Store) cacheStatsTokenId(token string, id int) { |
1452 | - s.cacheMu.Lock() |
1453 | - defer s.cacheMu.Unlock() |
1454 | - // Can't possibly be >, but reviews want it for defensiveness. |
1455 | - if len(s.statsTokenNew) >= statsTokenCacheSize { |
1456 | - s.statsTokenOld = s.statsTokenNew |
1457 | - s.statsTokenNew = nil |
1458 | - } |
1459 | - if s.statsTokenNew == nil { |
1460 | - s.statsTokenNew = make(map[string]int, statsTokenCacheSize) |
1461 | - } |
1462 | - s.statsTokenNew[token] = id |
1463 | -} |
1464 | - |
1465 | -// statsTokenId returns the id for token from the cache, if found. |
1466 | -func (s *Store) statsTokenId(token string) (id int, found bool) { |
1467 | - s.cacheMu.RLock() |
1468 | - id, found = s.statsTokenNew[token] |
1469 | - if found { |
1470 | - s.cacheMu.RUnlock() |
1471 | - return |
1472 | - } |
1473 | - id, found = s.statsTokenOld[token] |
1474 | - s.cacheMu.RUnlock() |
1475 | - if found { |
1476 | - s.cacheStatsTokenId(token, id) |
1477 | - } |
1478 | - return |
1479 | -} |
1480 | - |
1481 | -var counterEpoch = time.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC).Unix() |
1482 | - |
1483 | -// IncCounter increases by one the counter associated with the composed key. |
1484 | -func (s *Store) IncCounter(key []string) error { |
1485 | - session := s.session.Copy() |
1486 | - defer session.Close() |
1487 | - |
1488 | - skey, err := s.statsKey(session, key, true) |
1489 | - if err != nil { |
1490 | - return err |
1491 | - } |
1492 | - |
1493 | - t := time.Now().UTC() |
1494 | - // Round to the start of the minute so we get one document per minute at most. |
1495 | - t = t.Add(-time.Duration(t.Second()) * time.Second) |
1496 | - counters := session.StatCounters() |
1497 | - _, err = counters.Upsert(bson.D{{"k", skey}, {"t", int32(t.Unix() - counterEpoch)}}, bson.D{{"$inc", bson.D{{"c", 1}}}}) |
1498 | - return err |
1499 | -} |
1500 | - |
1501 | -// SumCounter returns the sum of all the counters that exactly match key, |
1502 | -// or that are prefixed by it if prefix is true. |
1503 | -func (s *Store) SumCounter(key []string, prefix bool) (count int64, err error) { |
1504 | - session := s.session.Copy() |
1505 | - defer session.Close() |
1506 | - |
1507 | - skey, err := s.statsKey(session, key, false) |
1508 | - if err == ErrNotFound { |
1509 | - return 0, nil |
1510 | - } |
1511 | - if err != nil { |
1512 | - return 0, err |
1513 | - } |
1514 | - |
1515 | - var regex string |
1516 | - if prefix { |
1517 | - regex = "^" + skey |
1518 | - } else { |
1519 | - regex = "^" + skey + "$" |
1520 | - } |
1521 | - |
1522 | - job := mgo.MapReduce{ |
1523 | - Map: "function() { emit('count', this.c); }", |
1524 | - Reduce: "function(key, values) { return Array.sum(values); }", |
1525 | - } |
1526 | - var result []struct{ Value int64 } |
1527 | - counters := session.StatCounters() |
1528 | - _, err = counters.Find(bson.D{{"k", bson.D{{"$regex", regex}}}}).MapReduce(&job, &result) |
1529 | - if len(result) > 0 { |
1530 | - return result[0].Value, err |
1531 | - } |
1532 | - return 0, err |
1533 | -} |
1534 | - |
1535 | -// A CharmPublisher is responsible for importing a charm dir onto the store. |
1536 | -type CharmPublisher struct { |
1537 | - revision int |
1538 | - w *charmWriter |
1539 | -} |
1540 | - |
1541 | -// Revision returns the revision that will be assigned to the published charm. |
1542 | -func (p *CharmPublisher) Revision() int { |
1543 | - return p.revision |
1544 | -} |
1545 | - |
1546 | -// CharmDir matches the part of the interface of *charm.Dir that is necessary |
1547 | -// to publish a charm. Using this interface rather than *charm.Dir directly |
1548 | -// makes testing some aspects of the store possible. |
1549 | -type CharmDir interface { |
1550 | - Meta() *charm.Meta |
1551 | - Config() *charm.Config |
1552 | - SetRevision(revision int) |
1553 | - BundleTo(w io.Writer) error |
1554 | -} |
1555 | - |
1556 | -// Statically ensure that *charm.Dir is indeed a CharmDir. |
1557 | -var _ CharmDir = (*charm.Dir)(nil) |
1558 | - |
1559 | -// Publish bundles charm and writes it to the store. The written charm |
1560 | -// bundle will have its revision set to the result of Revision. |
1561 | -// Publish must be called only once for a CharmPublisher. |
1562 | -func (p *CharmPublisher) Publish(charm CharmDir) error { |
1563 | - w := p.w |
1564 | - if w == nil { |
1565 | - panic("CharmPublisher already published a charm") |
1566 | - } |
1567 | - p.w = nil |
1568 | - w.charm = charm |
1569 | - // TODO: Refactor to BundleTo(w, revision) |
1570 | - charm.SetRevision(p.revision) |
1571 | - err := charm.BundleTo(w) |
1572 | - if err == nil { |
1573 | - err = w.finish() |
1574 | - } else { |
1575 | - w.abort() |
1576 | - } |
1577 | - return err |
1578 | -} |
1579 | - |
1580 | -// CharmPublisher returns a new CharmPublisher for importing a charm that |
1581 | -// will be made available in the store at all of the provided URLs. |
1582 | -// The digest parameter must contain the unique identifier that |
1583 | -// represents the charm data being imported (e.g. the VCS revision sha1). |
1584 | -// ErrRedundantUpdate is returned if all of the provided urls are |
1585 | -// already associated to that digest. |
1586 | -func (s *Store) CharmPublisher(urls []*charm.URL, digest string) (p *CharmPublisher, err error) { |
1587 | - log.Printf("store: Trying to add charms %v with key %q...", urls, digest) |
1588 | - if err = mustLackRevision("CharmPublisher", urls...); err != nil { |
1589 | - return |
1590 | - } |
1591 | - session := s.session.Copy() |
1592 | - defer session.Close() |
1593 | - |
1594 | - maxRev := -1 |
1595 | - newKey := false |
1596 | - charms := session.Charms() |
1597 | - doc := charmDoc{} |
1598 | - for i := range urls { |
1599 | - urlStr := urls[i].String() |
1600 | - err = charms.Find(bson.D{{"urls", urlStr}}).Sort("-revision").One(&doc) |
1601 | - if err == mgo.ErrNotFound { |
1602 | - log.Printf("store: Charm %s not yet in the store.", urls[i]) |
1603 | - newKey = true |
1604 | - continue |
1605 | - } |
1606 | - if doc.Digest != digest { |
1607 | - log.Printf("store: Charm %s is out of date with revision key %q.", urlStr, digest) |
1608 | - newKey = true |
1609 | - } |
1610 | - if err != nil { |
1611 | - log.Printf("store: Unknown error looking for charm %s: %s", urlStr, err) |
1612 | - return |
1613 | - } |
1614 | - if doc.Revision > maxRev { |
1615 | - maxRev = doc.Revision |
1616 | - } |
1617 | - } |
1618 | - if !newKey { |
1619 | - log.Printf("store: All charms have revision key %q. Nothing to update.", digest) |
1620 | - err = ErrRedundantUpdate |
1621 | - return |
1622 | - } |
1623 | - revision := maxRev + 1 |
1624 | - log.Printf("store: Preparing writer to add charms with revision %d.", revision) |
1625 | - w := &charmWriter{ |
1626 | - store: s, |
1627 | - urls: urls, |
1628 | - revision: revision, |
1629 | - digest: digest, |
1630 | - } |
1631 | - return &CharmPublisher{revision, w}, nil |
1632 | -} |
1633 | - |
1634 | -// charmWriter is an io.Writer that writes charm bundles to the charms GridFS. |
1635 | -type charmWriter struct { |
1636 | - store *Store |
1637 | - session *storeSession |
1638 | - file *mgo.GridFile |
1639 | - sha256 hash.Hash |
1640 | - charm CharmDir |
1641 | - urls []*charm.URL |
1642 | - revision int |
1643 | - digest string |
1644 | -} |
1645 | - |
1646 | -// Write creates an entry in the charms GridFS when first called, |
1647 | -// and streams all written data into it. |
1648 | -func (w *charmWriter) Write(data []byte) (n int, err error) { |
1649 | - if w.file == nil { |
1650 | - w.session = w.store.session.Copy() |
1651 | - w.file, err = w.session.CharmFS().Create("") |
1652 | - if err != nil { |
1653 | - log.Printf("store: Failed to create GridFS file: %v", err) |
1654 | - return 0, err |
1655 | - } |
1656 | - w.sha256 = sha256.New() |
1657 | - log.Printf("store: Creating GridFS file with id %q...", w.file.Id().(bson.ObjectId).Hex()) |
1658 | - } |
1659 | - _, err = w.sha256.Write(data) |
1660 | - if err != nil { |
1661 | - panic("hash.Hash should never error") |
1662 | - } |
1663 | - return w.file.Write(data) |
1664 | -} |
1665 | - |
1666 | -// abort cancels the charm writing. |
1667 | -func (w *charmWriter) abort() { |
1668 | - if w.file != nil { |
1669 | - // Ignore error. Already aborting due to a preceding bad situation |
1670 | - // elsewhere. This error is not important right now. |
1671 | - _ = w.file.Close() |
1672 | - w.session.Close() |
1673 | - } |
1674 | -} |
1675 | - |
1676 | -// finish completes the charm writing process and inserts the final metadata. |
1677 | -// After it completes the charm will be available for consumption. |
1678 | -func (w *charmWriter) finish() error { |
1679 | - if w.file == nil { |
1680 | - return nil |
1681 | - } |
1682 | - defer w.session.Close() |
1683 | - id := w.file.Id() |
1684 | - size := w.file.Size() |
1685 | - err := w.file.Close() |
1686 | - if err != nil { |
1687 | - log.Printf("store: Failed to close GridFS file: %v", err) |
1688 | - return err |
1689 | - } |
1690 | - charms := w.session.Charms() |
1691 | - sha256 := hex.EncodeToString(w.sha256.Sum(nil)) |
1692 | - charm := charmDoc{ |
1693 | - w.urls, |
1694 | - w.revision, |
1695 | - w.digest, |
1696 | - sha256, |
1697 | - size, |
1698 | - id.(bson.ObjectId), |
1699 | - w.charm.Meta(), |
1700 | - w.charm.Config(), |
1701 | - } |
1702 | - if err = charms.Insert(&charm); err != nil { |
1703 | - err = maybeConflict(err) |
1704 | - log.Printf("store: Failed to insert new revision of charm %v: %v", w.urls, err) |
1705 | - return err |
1706 | - } |
1707 | - return nil |
1708 | -} |
1709 | - |
1710 | -type CharmInfo struct { |
1711 | - revision int |
1712 | - digest string |
1713 | - sha256 string |
1714 | - size int64 |
1715 | - fileId bson.ObjectId |
1716 | - meta *charm.Meta |
1717 | - config *charm.Config |
1718 | -} |
1719 | - |
1720 | -// Statically ensure CharmInfo is a charm.Charm. |
1721 | -var _ charm.Charm = (*CharmInfo)(nil) |
1722 | - |
1723 | -// Revision returns the store charm's revision. |
1724 | -func (ci *CharmInfo) Revision() int { |
1725 | - return ci.revision |
1726 | -} |
1727 | - |
1728 | -// BundleSha256 returns the sha256 checksum for the stored charm bundle. |
1729 | -func (ci *CharmInfo) BundleSha256() string { |
1730 | - return ci.sha256 |
1731 | -} |
1732 | - |
1733 | -// BundleSize returns the size for the stored charm bundle. |
1734 | -func (ci *CharmInfo) BundleSize() int64 { |
1735 | - return ci.size |
1736 | -} |
1737 | - |
1738 | -// Digest returns the unique identifier that represents the charm |
1739 | -// data imported. This is typically set to the VCS revision digest. |
1740 | -func (ci *CharmInfo) Digest() string { |
1741 | - return ci.digest |
1742 | -} |
1743 | - |
1744 | -// Meta returns the charm.Meta details for the stored charm. |
1745 | -func (ci *CharmInfo) Meta() *charm.Meta { |
1746 | - return ci.meta |
1747 | -} |
1748 | - |
1749 | -// Config returns the charm.Config details for the stored charm. |
1750 | -func (ci *CharmInfo) Config() *charm.Config { |
1751 | - return ci.config |
1752 | -} |
1753 | - |
1754 | -// CharmInfo retrieves the CharmInfo value for the charm at url. |
1755 | -func (s *Store) CharmInfo(url *charm.URL) (info *CharmInfo, err error) { |
1756 | - session := s.session.Copy() |
1757 | - defer session.Close() |
1758 | - |
1759 | - log.Debugf("store: Retrieving charm info for %s", url) |
1760 | - rev := url.Revision |
1761 | - url = url.WithRevision(-1) |
1762 | - |
1763 | - charms := session.Charms() |
1764 | - var cdoc charmDoc |
1765 | - var qdoc interface{} |
1766 | - if rev == -1 { |
1767 | - qdoc = bson.D{{"urls", url}} |
1768 | - } else { |
1769 | - qdoc = bson.D{{"urls", url}, {"revision", rev}} |
1770 | - } |
1771 | - err = charms.Find(qdoc).Sort("-revision").One(&cdoc) |
1772 | - if err != nil { |
1773 | - log.Printf("store: Failed to find charm %s: %v", url, err) |
1774 | - return nil, ErrNotFound |
1775 | - } |
1776 | - info = &CharmInfo{ |
1777 | - cdoc.Revision, |
1778 | - cdoc.Digest, |
1779 | - cdoc.Sha256, |
1780 | - cdoc.Size, |
1781 | - cdoc.FileId, |
1782 | - cdoc.Meta, |
1783 | - cdoc.Config, |
1784 | - } |
1785 | - return info, nil |
1786 | -} |
1787 | - |
1788 | -// OpenCharm opens for reading via rc the charm currently available at url. |
1789 | -// rc must be closed after dealing with it or resources will leak. |
1790 | -func (s *Store) OpenCharm(url *charm.URL) (info *CharmInfo, rc io.ReadCloser, err error) { |
1791 | - log.Debugf("store: Opening charm %s", url) |
1792 | - info, err = s.CharmInfo(url) |
1793 | - if err != nil { |
1794 | - return nil, nil, err |
1795 | - } |
1796 | - session := s.session.Copy() |
1797 | - file, err := session.CharmFS().OpenId(info.fileId) |
1798 | - if err != nil { |
1799 | - log.Printf("store: Failed to open GridFS file for charm %s: %v", url, err) |
1800 | - session.Close() |
1801 | - return nil, nil, err |
1802 | - } |
1803 | - rc = &reader{session, file} |
1804 | - return |
1805 | -} |
1806 | - |
1807 | -type reader struct { |
1808 | - session *storeSession |
1809 | - file *mgo.GridFile |
1810 | -} |
1811 | - |
1812 | -// Read consumes data from the opened charm. |
1813 | -func (r *reader) Read(buf []byte) (n int, err error) { |
1814 | - return r.file.Read(buf) |
1815 | -} |
1816 | - |
1817 | -// Close closes the opened charm and frees associated resources. |
1818 | -func (r *reader) Close() error { |
1819 | - err := r.file.Close() |
1820 | - r.session.Close() |
1821 | - return err |
1822 | -} |
1823 | - |
1824 | -// charmDoc represents the document stored in MongoDB for a charm. |
1825 | -type charmDoc struct { |
1826 | - URLs []*charm.URL |
1827 | - Revision int |
1828 | - Digest string |
1829 | - Sha256 string |
1830 | - Size int64 |
1831 | - FileId bson.ObjectId |
1832 | - Meta *charm.Meta |
1833 | - Config *charm.Config |
1834 | -} |
1835 | - |
1836 | -// LockUpdates acquires a server-side lock for updating a single charm |
1837 | -// that is supposed to be made available in all of the provided urls. |
1838 | -// If the lock can't be acquired in any of the urls, an error will be |
1839 | -// immediately returned. |
1840 | -// In the usual case, any locking done is undone when an error happens, |
1841 | -// or when l.Unlock is called. If something else goes wrong, the locks |
1842 | -// will also expire after the period defined in UpdateTimeout. |
1843 | -func (s *Store) LockUpdates(urls []*charm.URL) (l *UpdateLock, err error) { |
1844 | - session := s.session.Copy() |
1845 | - keys := make([]string, len(urls)) |
1846 | - for i := range urls { |
1847 | - keys[i] = urls[i].String() |
1848 | - } |
1849 | - sort.Strings(keys) |
1850 | - l = &UpdateLock{keys, session.Locks(), bson.Now()} |
1851 | - if err = l.tryLock(); err != nil { |
1852 | - session.Close() |
1853 | - return nil, err |
1854 | - } |
1855 | - return l, nil |
1856 | -} |
1857 | - |
1858 | -// UpdateLock represents an acquired update lock over a set of charm URLs. |
1859 | -type UpdateLock struct { |
1860 | - keys []string |
1861 | - locks *mgo.Collection |
1862 | - time time.Time |
1863 | -} |
1864 | - |
1865 | -// Unlock removes the previously acquired server-side lock that prevents |
1866 | -// other processes from attempting to update a set of charm URLs. |
1867 | -func (l *UpdateLock) Unlock() { |
1868 | - log.Debugf("store: Unlocking charms for future updates: %v", l.keys) |
1869 | - defer l.locks.Database.Session.Close() |
1870 | - for i := len(l.keys) - 1; i >= 0; i-- { |
1871 | - // Using time below ensures only the proper lock is removed. |
1872 | - // Can't do much about errors here. Locks will expire anyway. |
1873 | - l.locks.Remove(bson.D{{"_id", l.keys[i]}, {"time", l.time}}) |
1874 | - } |
1875 | -} |
1876 | - |
1877 | -// tryLock tries locking l.keys, one at a time, and succeeds only if it |
1878 | -// can lock all of them in order. The keys should be pre-sorted so that |
1879 | -// two-way conflicts can't happen. If any of the keys fail to be locked, |
1880 | -// and expiring the old lock doesn't work, tryLock undoes all previous |
1881 | -// locks and aborts with an error. |
1882 | -func (l *UpdateLock) tryLock() error { |
1883 | - for i, key := range l.keys { |
1884 | - log.Debugf("store: Trying to lock charm %s for updates...", key) |
1885 | - doc := bson.D{{"_id", key}, {"time", l.time}} |
1886 | - err := l.locks.Insert(doc) |
1887 | - if err == nil { |
1888 | - log.Debugf("store: Charm %s is now locked for updates.", key) |
1889 | - continue |
1890 | - } |
1891 | - if lerr, ok := err.(*mgo.LastError); ok && lerr.Code == 11000 { |
1892 | - log.Debugf("store: Charm %s is locked. Trying to expire lock.", key) |
1893 | - l.tryExpire(key) |
1894 | - err = l.locks.Insert(doc) |
1895 | - if err == nil { |
1896 | - log.Debugf("store: Charm %s is now locked for updates.", key) |
1897 | - continue |
1898 | - } |
1899 | - } |
1900 | - // Couldn't lock everyone. Undo previous locks. |
1901 | - for j := i - 1; j >= 0; j-- { |
1902 | - // Using time below should be unnecessary, but it's an extra check. |
1903 | - // Can't do anything about errors here. Lock will expire anyway. |
1904 | - l.locks.Remove(bson.D{{"_id", l.keys[j]}, {"time", l.time}}) |
1905 | - } |
1906 | - err = maybeConflict(err) |
1907 | - log.Printf("store: Can't lock charms %v for updating: %v", l.keys, err) |
1908 | - return err |
1909 | - } |
1910 | - return nil |
1911 | -} |
1912 | - |
1913 | -// tryExpire attempts to remove outdated locks from the database. |
1914 | -func (l *UpdateLock) tryExpire(key string) { |
1915 | - // Ignore errors. If nothing happens the key will continue locked. |
1916 | - l.locks.Remove(bson.D{{"_id", key}, {"time", bson.D{{"$lt", bson.Now().Add(-UpdateTimeout)}}}}) |
1917 | -} |
1918 | - |
1919 | -// maybeConflict returns an ErrUpdateConflict if err is a mgo |
1920 | -// insert conflict LastError, or err itself otherwise. |
1921 | -func maybeConflict(err error) error { |
1922 | - if lerr, ok := err.(*mgo.LastError); ok && lerr.Code == 11000 { |
1923 | - return ErrUpdateConflict |
1924 | - } |
1925 | - return err |
1926 | -} |
1927 | - |
1928 | -// storeSession wraps a mgo.Session ands adds a few convenience methods. |
1929 | -type storeSession struct { |
1930 | - *mgo.Session |
1931 | -} |
1932 | - |
1933 | -// Copy copies the storeSession and its underlying mgo session. |
1934 | -func (s *storeSession) Copy() *storeSession { |
1935 | - return &storeSession{s.Session.Copy()} |
1936 | -} |
1937 | - |
1938 | -// Charms returns the mongo collection where charms are stored. |
1939 | -func (s *storeSession) Charms() *mgo.Collection { |
1940 | - return s.DB("juju").C("charms") |
1941 | -} |
1942 | - |
1943 | -// Charms returns a mgo.GridFS to read and write charms. |
1944 | -func (s *storeSession) CharmFS() *mgo.GridFS { |
1945 | - return s.DB("juju").GridFS("charmfs") |
1946 | -} |
1947 | - |
1948 | -// Events returns the mongo collection where charm events are stored. |
1949 | -func (s *storeSession) Events() *mgo.Collection { |
1950 | - return s.DB("juju").C("events") |
1951 | -} |
1952 | - |
1953 | -// Locks returns the mongo collection where charm locks are stored. |
1954 | -func (s *storeSession) Locks() *mgo.Collection { |
1955 | - return s.DB("juju").C("locks") |
1956 | -} |
1957 | - |
1958 | -// StatTokens returns the mongo collection for storing key tokens |
1959 | -// for statistics collection. |
1960 | -func (s *storeSession) StatTokens() *mgo.Collection { |
1961 | - return s.DB("juju").C("stat.tokens") |
1962 | -} |
1963 | - |
1964 | -// StatCounters returns the mongo collection for counter values. |
1965 | -func (s *storeSession) StatCounters() *mgo.Collection { |
1966 | - return s.DB("juju").C("stat.counters") |
1967 | -} |
1968 | - |
1969 | -type CharmEventKind int |
1970 | - |
1971 | -const ( |
1972 | - EventPublished CharmEventKind = iota + 1 |
1973 | - EventPublishError |
1974 | - |
1975 | - EventKindCount |
1976 | -) |
1977 | - |
1978 | -func (k CharmEventKind) String() string { |
1979 | - switch k { |
1980 | - case EventPublished: |
1981 | - return "published" |
1982 | - case EventPublishError: |
1983 | - return "publish-error" |
1984 | - } |
1985 | - panic(fmt.Errorf("unknown charm event kind %d", k)) |
1986 | -} |
1987 | - |
1988 | -// CharmEvent is a record for an event relating to one or more charm URLs. |
1989 | -type CharmEvent struct { |
1990 | - Kind CharmEventKind |
1991 | - Digest string |
1992 | - Revision int |
1993 | - URLs []*charm.URL |
1994 | - Errors []string `bson:",omitempty"` |
1995 | - Warnings []string `bson:",omitempty"` |
1996 | - Time time.Time |
1997 | -} |
1998 | - |
1999 | -// LogCharmEvent records an event related to one or more charm URLs. |
2000 | -func (s *Store) LogCharmEvent(event *CharmEvent) (err error) { |
2001 | - log.Printf("store: Adding charm event for %v with key %q: %s", event.URLs, event.Digest, event.Kind) |
2002 | - if err = mustLackRevision("LogCharmEvent", event.URLs...); err != nil { |
2003 | - return |
2004 | - } |
2005 | - session := s.session.Copy() |
2006 | - defer session.Close() |
2007 | - if event.Kind == 0 || event.Digest == "" || len(event.URLs) == 0 { |
2008 | - return fmt.Errorf("LogCharmEvent: need valid Kind, Digest and URLs") |
2009 | - } |
2010 | - if event.Time.IsZero() { |
2011 | - event.Time = time.Now() |
2012 | - } |
2013 | - events := session.Events() |
2014 | - return events.Insert(event) |
2015 | -} |
2016 | - |
2017 | -// CharmEvent returns the most recent event associated with url |
2018 | -// and digest. If the specified event isn't found the error |
2019 | -// ErrUnknownChange will be returned. |
2020 | -func (s *Store) CharmEvent(url *charm.URL, digest string) (*CharmEvent, error) { |
2021 | - // TODO: It'd actually make sense to find the charm event after the |
2022 | - // revision id, but since we don't care about that now, just make sure |
2023 | - // we don't write bad code. |
2024 | - if err := mustLackRevision("CharmEvent", url); err != nil { |
2025 | - return nil, err |
2026 | - } |
2027 | - session := s.session.Copy() |
2028 | - defer session.Close() |
2029 | - |
2030 | - events := session.Events() |
2031 | - event := &CharmEvent{Digest: digest} |
2032 | - query := events.Find(bson.D{{"urls", url}, {"digest", digest}}) |
2033 | - query.Sort("-time") |
2034 | - err := query.One(&event) |
2035 | - if err == mgo.ErrNotFound { |
2036 | - return nil, ErrNotFound |
2037 | - } |
2038 | - if err != nil { |
2039 | - return nil, err |
2040 | - } |
2041 | - return event, nil |
2042 | -} |
2043 | - |
2044 | -// mustLackRevision returns an error if any of the urls has a revision. |
2045 | -func mustLackRevision(context string, urls ...*charm.URL) error { |
2046 | - for _, url := range urls { |
2047 | - if url.Revision != -1 { |
2048 | - err := fmt.Errorf("%s: got charm URL with revision: %s", context, url) |
2049 | - log.Printf("store: %v", err) |
2050 | - return err |
2051 | - } |
2052 | - } |
2053 | - return nil |
2054 | -} |
2055 | |
2056 | === removed file 'store/store_test.go' |
2057 | --- store/store_test.go 2012-11-20 07:18:32 +0000 |
2058 | +++ store/store_test.go 1970-01-01 00:00:00 +0000 |
2059 | @@ -1,608 +0,0 @@ |
2060 | -package store_test |
2061 | - |
2062 | -import ( |
2063 | - "fmt" |
2064 | - "io" |
2065 | - "io/ioutil" |
2066 | - "labix.org/v2/mgo/bson" |
2067 | - . "launchpad.net/gocheck" |
2068 | - "launchpad.net/juju-core/charm" |
2069 | - "launchpad.net/juju-core/log" |
2070 | - "launchpad.net/juju-core/store" |
2071 | - "launchpad.net/juju-core/testing" |
2072 | - "strconv" |
2073 | - "sync" |
2074 | - stdtesting "testing" |
2075 | - "time" |
2076 | -) |
2077 | - |
2078 | -func Test(t *stdtesting.T) { |
2079 | - TestingT(t) |
2080 | -} |
2081 | - |
2082 | -var _ = Suite(&StoreSuite{}) |
2083 | -var _ = Suite(&TrivialSuite{}) |
2084 | - |
2085 | -type StoreSuite struct { |
2086 | - MgoSuite |
2087 | - testing.HTTPSuite |
2088 | - store *store.Store |
2089 | -} |
2090 | - |
2091 | -type TrivialSuite struct{} |
2092 | - |
2093 | -func (s *StoreSuite) SetUpSuite(c *C) { |
2094 | - s.MgoSuite.SetUpSuite(c) |
2095 | - s.HTTPSuite.SetUpSuite(c) |
2096 | -} |
2097 | - |
2098 | -func (s *StoreSuite) TearDownSuite(c *C) { |
2099 | - s.HTTPSuite.TearDownSuite(c) |
2100 | - s.MgoSuite.TearDownSuite(c) |
2101 | -} |
2102 | - |
2103 | -func (s *StoreSuite) SetUpTest(c *C) { |
2104 | - s.MgoSuite.SetUpTest(c) |
2105 | - var err error |
2106 | - s.store, err = store.Open(s.Addr) |
2107 | - c.Assert(err, IsNil) |
2108 | - log.Target = c |
2109 | - log.Debug = true |
2110 | -} |
2111 | - |
2112 | -func (s *StoreSuite) TearDownTest(c *C) { |
2113 | - s.HTTPSuite.TearDownTest(c) |
2114 | - if s.store != nil { |
2115 | - s.store.Close() |
2116 | - } |
2117 | - s.MgoSuite.TearDownTest(c) |
2118 | -} |
2119 | - |
2120 | -// FakeCharmDir is a charm that implements the interface that the |
2121 | -// store publisher cares about. |
2122 | -type FakeCharmDir struct { |
2123 | - revision interface{} // so we can tell if it's not set. |
2124 | - error string |
2125 | -} |
2126 | - |
2127 | -func (d *FakeCharmDir) Meta() *charm.Meta { |
2128 | - return &charm.Meta{ |
2129 | - Name: "fakecharm", |
2130 | - Summary: "Fake charm for testing purposes.", |
2131 | - Description: "This is a fake charm for testing purposes.\n", |
2132 | - Provides: make(map[string]charm.Relation), |
2133 | - Requires: make(map[string]charm.Relation), |
2134 | - Peers: make(map[string]charm.Relation), |
2135 | - } |
2136 | -} |
2137 | - |
2138 | -func (d *FakeCharmDir) Config() *charm.Config { |
2139 | - return &charm.Config{make(map[string]charm.Option)} |
2140 | -} |
2141 | - |
2142 | -func (d *FakeCharmDir) SetRevision(revision int) { |
2143 | - d.revision = revision |
2144 | -} |
2145 | - |
2146 | -func (d *FakeCharmDir) BundleTo(w io.Writer) error { |
2147 | - if d.error == "beforeWrite" { |
2148 | - return fmt.Errorf(d.error) |
2149 | - } |
2150 | - _, err := w.Write([]byte(fmt.Sprintf("charm-revision-%v", d.revision))) |
2151 | - if d.error == "afterWrite" { |
2152 | - return fmt.Errorf(d.error) |
2153 | - } |
2154 | - return err |
2155 | -} |
2156 | - |
2157 | -func (s *StoreSuite) TestCharmPublisherWithRevisionedURL(c *C) { |
2158 | - urls := []*charm.URL{charm.MustParseURL("cs:oneiric/wordpress-0")} |
2159 | - pub, err := s.store.CharmPublisher(urls, "some-digest") |
2160 | - c.Assert(err, ErrorMatches, "CharmPublisher: got charm URL with revision: cs:oneiric/wordpress-0") |
2161 | - c.Assert(pub, IsNil) |
2162 | -} |
2163 | - |
2164 | -func (s *StoreSuite) TestCharmPublisher(c *C) { |
2165 | - urlA := charm.MustParseURL("cs:oneiric/wordpress-a") |
2166 | - urlB := charm.MustParseURL("cs:oneiric/wordpress-b") |
2167 | - urls := []*charm.URL{urlA, urlB} |
2168 | - |
2169 | - pub, err := s.store.CharmPublisher(urls, "some-digest") |
2170 | - c.Assert(err, IsNil) |
2171 | - c.Assert(pub.Revision(), Equals, 0) |
2172 | - |
2173 | - err = pub.Publish(testing.Charms.ClonedDir(c.MkDir(), "series", "dummy")) |
2174 | - c.Assert(err, IsNil) |
2175 | - |
2176 | - for _, url := range urls { |
2177 | - info, rc, err := s.store.OpenCharm(url) |
2178 | - c.Assert(err, IsNil) |
2179 | - c.Assert(info.Revision(), Equals, 0) |
2180 | - c.Assert(info.Digest(), Equals, "some-digest") |
2181 | - data, err := ioutil.ReadAll(rc) |
2182 | - c.Check(err, IsNil) |
2183 | - err = rc.Close() |
2184 | - c.Assert(err, IsNil) |
2185 | - bundle, err := charm.ReadBundleBytes(data) |
2186 | - c.Assert(err, IsNil) |
2187 | - |
2188 | - // The same information must be available by reading the |
2189 | - // full charm data... |
2190 | - c.Assert(bundle.Meta().Name, Equals, "dummy") |
2191 | - c.Assert(bundle.Config().Options["title"].Default, Equals, "My Title") |
2192 | - |
2193 | - // ... and the queriable details. |
2194 | - c.Assert(info.Meta().Name, Equals, "dummy") |
2195 | - c.Assert(info.Config().Options["title"].Default, Equals, "My Title") |
2196 | - |
2197 | - info2, err := s.store.CharmInfo(url) |
2198 | - c.Assert(err, IsNil) |
2199 | - c.Assert(info2, DeepEquals, info) |
2200 | - } |
2201 | -} |
2202 | - |
2203 | -func (s *StoreSuite) TestCharmPublishError(c *C) { |
2204 | - url := charm.MustParseURL("cs:oneiric/wordpress") |
2205 | - urls := []*charm.URL{url} |
2206 | - |
2207 | - // Publish one successfully to bump the revision so we can |
2208 | - // make sure it is being correctly set below. |
2209 | - pub, err := s.store.CharmPublisher(urls, "one-digest") |
2210 | - c.Assert(err, IsNil) |
2211 | - c.Assert(pub.Revision(), Equals, 0) |
2212 | - err = pub.Publish(&FakeCharmDir{}) |
2213 | - c.Assert(err, IsNil) |
2214 | - |
2215 | - pub, err = s.store.CharmPublisher(urls, "another-digest") |
2216 | - c.Assert(err, IsNil) |
2217 | - c.Assert(pub.Revision(), Equals, 1) |
2218 | - err = pub.Publish(&FakeCharmDir{error: "beforeWrite"}) |
2219 | - c.Assert(err, ErrorMatches, "beforeWrite") |
2220 | - |
2221 | - pub, err = s.store.CharmPublisher(urls, "another-digest") |
2222 | - c.Assert(err, IsNil) |
2223 | - c.Assert(pub.Revision(), Equals, 1) |
2224 | - err = pub.Publish(&FakeCharmDir{error: "afterWrite"}) |
2225 | - c.Assert(err, ErrorMatches, "afterWrite") |
2226 | - |
2227 | - // Still at the original charm revision that succeeded first. |
2228 | - info, err := s.store.CharmInfo(url) |
2229 | - c.Assert(err, IsNil) |
2230 | - c.Assert(info.Revision(), Equals, 0) |
2231 | - c.Assert(info.Digest(), Equals, "one-digest") |
2232 | -} |
2233 | - |
2234 | -func (s *StoreSuite) TestCharmInfoNotFound(c *C) { |
2235 | - info, err := s.store.CharmInfo(charm.MustParseURL("cs:oneiric/wordpress")) |
2236 | - c.Assert(err, Equals, store.ErrNotFound) |
2237 | - c.Assert(info, IsNil) |
2238 | -} |
2239 | - |
2240 | -func (s *StoreSuite) TestRevisioning(c *C) { |
2241 | - urlA := charm.MustParseURL("cs:oneiric/wordpress-a") |
2242 | - urlB := charm.MustParseURL("cs:oneiric/wordpress-b") |
2243 | - urls := []*charm.URL{urlA, urlB} |
2244 | - |
2245 | - tests := []struct { |
2246 | - urls []*charm.URL |
2247 | - data string |
2248 | - }{ |
2249 | - {urls[0:], "charm-revision-0"}, |
2250 | - {urls[1:], "charm-revision-1"}, |
2251 | - {urls[0:], "charm-revision-2"}, |
2252 | - } |
2253 | - |
2254 | - for i, t := range tests { |
2255 | - pub, err := s.store.CharmPublisher(t.urls, fmt.Sprintf("digest-%d", i)) |
2256 | - c.Assert(err, IsNil) |
2257 | - c.Assert(pub.Revision(), Equals, i) |
2258 | - |
2259 | - err = pub.Publish(&FakeCharmDir{}) |
2260 | - c.Assert(err, IsNil) |
2261 | - } |
2262 | - |
2263 | - for i, t := range tests { |
2264 | - for _, url := range t.urls { |
2265 | - url = url.WithRevision(i) |
2266 | - info, rc, err := s.store.OpenCharm(url) |
2267 | - c.Assert(err, IsNil) |
2268 | - data, err := ioutil.ReadAll(rc) |
2269 | - cerr := rc.Close() |
2270 | - c.Assert(info.Revision(), Equals, i) |
2271 | - c.Assert(url.Revision, Equals, i) // Untouched. |
2272 | - c.Assert(cerr, IsNil) |
2273 | - c.Assert(string(data), Equals, string(t.data)) |
2274 | - c.Assert(err, IsNil) |
2275 | - } |
2276 | - } |
2277 | - |
2278 | - info, rc, err := s.store.OpenCharm(urlA.WithRevision(1)) |
2279 | - c.Assert(err, Equals, store.ErrNotFound) |
2280 | - c.Assert(info, IsNil) |
2281 | - c.Assert(rc, IsNil) |
2282 | -} |
2283 | - |
2284 | -func (s *StoreSuite) TestLockUpdates(c *C) { |
2285 | - urlA := charm.MustParseURL("cs:oneiric/wordpress-a") |
2286 | - urlB := charm.MustParseURL("cs:oneiric/wordpress-b") |
2287 | - urls := []*charm.URL{urlA, urlB} |
2288 | - |
2289 | - // Lock update of just B to force a partial conflict. |
2290 | - lock1, err := s.store.LockUpdates(urls[1:]) |
2291 | - c.Assert(err, IsNil) |
2292 | - |
2293 | - // Partially conflicts with locked update above. |
2294 | - lock2, err := s.store.LockUpdates(urls) |
2295 | - c.Check(err, Equals, store.ErrUpdateConflict) |
2296 | - c.Check(lock2, IsNil) |
2297 | - |
2298 | - lock1.Unlock() |
2299 | - |
2300 | - // Trying again should work since lock1 was released. |
2301 | - lock3, err := s.store.LockUpdates(urls) |
2302 | - c.Assert(err, IsNil) |
2303 | - lock3.Unlock() |
2304 | -} |
2305 | - |
2306 | -func (s *StoreSuite) TestLockUpdatesExpires(c *C) { |
2307 | - urlA := charm.MustParseURL("cs:oneiric/wordpress-a") |
2308 | - urlB := charm.MustParseURL("cs:oneiric/wordpress-b") |
2309 | - urls := []*charm.URL{urlA, urlB} |
2310 | - |
2311 | - // Initiate an update of B only to force a partial conflict. |
2312 | - lock1, err := s.store.LockUpdates(urls[1:]) |
2313 | - c.Assert(err, IsNil) |
2314 | - |
2315 | - // Hack time to force an expiration. |
2316 | - locks := s.Session.DB("juju").C("locks") |
2317 | - selector := bson.M{"_id": urlB.String()} |
2318 | - update := bson.M{"time": bson.Now().Add(-store.UpdateTimeout - 10e9)} |
2319 | - err = locks.Update(selector, update) |
2320 | - c.Check(err, IsNil) |
2321 | - |
2322 | - // Works due to expiration of previous lock. |
2323 | - lock2, err := s.store.LockUpdates(urls) |
2324 | - c.Assert(err, IsNil) |
2325 | - defer lock2.Unlock() |
2326 | - |
2327 | - // The expired lock was forcefully killed. Unlocking it must |
2328 | - // not interfere with lock2 which is still alive. |
2329 | - lock1.Unlock() |
2330 | - |
2331 | - // The above statement was a NOOP and lock2 is still in effect, |
2332 | - // so attempting another lock must necessarily fail. |
2333 | - lock3, err := s.store.LockUpdates(urls) |
2334 | - c.Check(err == store.ErrUpdateConflict, Equals, true) |
2335 | - c.Check(lock3, IsNil) |
2336 | -} |
2337 | - |
2338 | -func (s *StoreSuite) TestConflictingUpdate(c *C) { |
2339 | - // This test checks that if for whatever reason the locking |
2340 | - // safety-net fails, adding two charms in parallel still |
2341 | - // results in a sane outcome. |
2342 | - url := charm.MustParseURL("cs:oneiric/wordpress") |
2343 | - urls := []*charm.URL{url} |
2344 | - |
2345 | - pub1, err := s.store.CharmPublisher(urls, "some-digest") |
2346 | - c.Assert(err, IsNil) |
2347 | - c.Assert(pub1.Revision(), Equals, 0) |
2348 | - |
2349 | - pub2, err := s.store.CharmPublisher(urls, "some-digest") |
2350 | - c.Assert(err, IsNil) |
2351 | - c.Assert(pub2.Revision(), Equals, 0) |
2352 | - |
2353 | - // The first publishing attempt should work. |
2354 | - err = pub2.Publish(&FakeCharmDir{}) |
2355 | - c.Assert(err, IsNil) |
2356 | - |
2357 | - // Attempting to finish the second attempt should break, |
2358 | - // since it lost the race and the given revision is already |
2359 | - // in place. |
2360 | - err = pub1.Publish(&FakeCharmDir{}) |
2361 | - c.Assert(err, Equals, store.ErrUpdateConflict) |
2362 | -} |
2363 | - |
2364 | -func (s *StoreSuite) TestRedundantUpdate(c *C) { |
2365 | - urlA := charm.MustParseURL("cs:oneiric/wordpress-a") |
2366 | - urlB := charm.MustParseURL("cs:oneiric/wordpress-b") |
2367 | - urls := []*charm.URL{urlA, urlB} |
2368 | - |
2369 | - pub, err := s.store.CharmPublisher(urls, "digest-0") |
2370 | - c.Assert(err, IsNil) |
2371 | - c.Assert(pub.Revision(), Equals, 0) |
2372 | - err = pub.Publish(&FakeCharmDir{}) |
2373 | - c.Assert(err, IsNil) |
2374 | - |
2375 | - // All charms are already on digest-0. |
2376 | - pub, err = s.store.CharmPublisher(urls, "digest-0") |
2377 | - c.Assert(err, ErrorMatches, "charm is up-to-date") |
2378 | - c.Assert(err, Equals, store.ErrRedundantUpdate) |
2379 | - c.Assert(pub, IsNil) |
2380 | - |
2381 | - // Now add a second revision just for wordpress-b. |
2382 | - pub, err = s.store.CharmPublisher(urls[1:], "digest-1") |
2383 | - c.Assert(err, IsNil) |
2384 | - c.Assert(pub.Revision(), Equals, 1) |
2385 | - err = pub.Publish(&FakeCharmDir{}) |
2386 | - c.Assert(err, IsNil) |
2387 | - |
2388 | - // Same digest bumps revision because one of them was old. |
2389 | - pub, err = s.store.CharmPublisher(urls, "digest-1") |
2390 | - c.Assert(err, IsNil) |
2391 | - c.Assert(pub.Revision(), Equals, 2) |
2392 | - err = pub.Publish(&FakeCharmDir{}) |
2393 | - c.Assert(err, IsNil) |
2394 | -} |
2395 | - |
2396 | -const fakeRevZeroSha = "319095521ac8a62fa1e8423351973512ecca8928c9f62025e37de57c9ef07a53" |
2397 | - |
2398 | -func (s *StoreSuite) TestCharmBundleData(c *C) { |
2399 | - url := charm.MustParseURL("cs:oneiric/wordpress") |
2400 | - urls := []*charm.URL{url} |
2401 | - |
2402 | - pub, err := s.store.CharmPublisher(urls, "key") |
2403 | - c.Assert(err, IsNil) |
2404 | - c.Assert(pub.Revision(), Equals, 0) |
2405 | - |
2406 | - err = pub.Publish(&FakeCharmDir{}) |
2407 | - c.Assert(err, IsNil) |
2408 | - |
2409 | - info, rc, err := s.store.OpenCharm(url) |
2410 | - c.Assert(err, IsNil) |
2411 | - c.Check(info.BundleSha256(), Equals, fakeRevZeroSha) |
2412 | - c.Check(info.BundleSize(), Equals, int64(len("charm-revision-0"))) |
2413 | - err = rc.Close() |
2414 | - c.Check(err, IsNil) |
2415 | -} |
2416 | - |
2417 | -func (s *StoreSuite) TestLogCharmEventWithRevisionedURL(c *C) { |
2418 | - url := charm.MustParseURL("cs:oneiric/wordpress-0") |
2419 | - event := &store.CharmEvent{ |
2420 | - Kind: store.EventPublishError, |
2421 | - Digest: "some-digest", |
2422 | - URLs: []*charm.URL{url}, |
2423 | - } |
2424 | - err := s.store.LogCharmEvent(event) |
2425 | - c.Assert(err, ErrorMatches, "LogCharmEvent: got charm URL with revision: cs:oneiric/wordpress-0") |
2426 | - |
2427 | - // This may work in the future, but not now. |
2428 | - event, err = s.store.CharmEvent(url, "some-digest") |
2429 | - c.Assert(err, ErrorMatches, "CharmEvent: got charm URL with revision: cs:oneiric/wordpress-0") |
2430 | - c.Assert(event, IsNil) |
2431 | -} |
2432 | - |
2433 | -func (s *StoreSuite) TestLogCharmEvent(c *C) { |
2434 | - url1 := charm.MustParseURL("cs:oneiric/wordpress") |
2435 | - url2 := charm.MustParseURL("cs:oneiric/mysql") |
2436 | - urls := []*charm.URL{url1, url2} |
2437 | - |
2438 | - event1 := &store.CharmEvent{ |
2439 | - Kind: store.EventPublished, |
2440 | - Revision: 42, |
2441 | - Digest: "revKey1", |
2442 | - URLs: urls, |
2443 | - Warnings: []string{"A warning."}, |
2444 | - Time: time.Unix(1, 0), |
2445 | - } |
2446 | - event2 := &store.CharmEvent{ |
2447 | - Kind: store.EventPublished, |
2448 | - Revision: 42, |
2449 | - Digest: "revKey2", |
2450 | - URLs: urls, |
2451 | - Time: time.Unix(1, 0), |
2452 | - } |
2453 | - event3 := &store.CharmEvent{ |
2454 | - Kind: store.EventPublishError, |
2455 | - Digest: "revKey2", |
2456 | - Errors: []string{"An error."}, |
2457 | - URLs: urls[:1], |
2458 | - } |
2459 | - |
2460 | - for _, event := range []*store.CharmEvent{event1, event2, event3} { |
2461 | - err := s.store.LogCharmEvent(event) |
2462 | - c.Assert(err, IsNil) |
2463 | - } |
2464 | - |
2465 | - events := s.Session.DB("juju").C("events") |
2466 | - var s1, s2 map[string]interface{} |
2467 | - |
2468 | - err := events.Find(bson.M{"digest": "revKey1"}).One(&s1) |
2469 | - c.Assert(err, IsNil) |
2470 | - c.Assert(s1["kind"], Equals, int(store.EventPublished)) |
2471 | - c.Assert(s1["urls"], DeepEquals, []interface{}{"cs:oneiric/wordpress", "cs:oneiric/mysql"}) |
2472 | - c.Assert(s1["warnings"], DeepEquals, []interface{}{"A warning."}) |
2473 | - c.Assert(s1["errors"], IsNil) |
2474 | - c.Assert(s1["time"], DeepEquals, time.Unix(1, 0)) |
2475 | - |
2476 | - err = events.Find(bson.M{"digest": "revKey2", "kind": store.EventPublishError}).One(&s2) |
2477 | - c.Assert(err, IsNil) |
2478 | - c.Assert(s2["urls"], DeepEquals, []interface{}{"cs:oneiric/wordpress"}) |
2479 | - c.Assert(s2["warnings"], IsNil) |
2480 | - c.Assert(s2["errors"], DeepEquals, []interface{}{"An error."}) |
2481 | - c.Assert(s2["time"].(time.Time).After(bson.Now().Add(-10e9)), Equals, true) |
2482 | - |
2483 | - // Mongo stores timestamps in milliseconds, so chop |
2484 | - // off the extra bits for comparison. |
2485 | - event3.Time = time.Unix(0, event3.Time.UnixNano()/1e6*1e6) |
2486 | - |
2487 | - event, err := s.store.CharmEvent(urls[0], "revKey2") |
2488 | - c.Assert(err, IsNil) |
2489 | - c.Assert(event, DeepEquals, event3) |
2490 | - |
2491 | - event, err = s.store.CharmEvent(urls[1], "revKey1") |
2492 | - c.Assert(err, IsNil) |
2493 | - c.Assert(event, DeepEquals, event1) |
2494 | - |
2495 | - event, err = s.store.CharmEvent(urls[1], "revKeyX") |
2496 | - c.Assert(err, Equals, store.ErrNotFound) |
2497 | - c.Assert(event, IsNil) |
2498 | -} |
2499 | - |
2500 | -func (s *StoreSuite) TestCounters(c *C) { |
2501 | - sum, err := s.store.SumCounter([]string{"a"}, false) |
2502 | - c.Assert(err, IsNil) |
2503 | - c.Assert(sum, Equals, int64(0)) |
2504 | - |
2505 | - for i := 0; i < 10; i++ { |
2506 | - err := s.store.IncCounter([]string{"a", "b", "c"}) |
2507 | - c.Assert(err, IsNil) |
2508 | - } |
2509 | - for i := 0; i < 7; i++ { |
2510 | - s.store.IncCounter([]string{"a", "b"}) |
2511 | - c.Assert(err, IsNil) |
2512 | - } |
2513 | - for i := 0; i < 3; i++ { |
2514 | - s.store.IncCounter([]string{"a", "z", "b"}) |
2515 | - c.Assert(err, IsNil) |
2516 | - } |
2517 | - |
2518 | - tests := []struct { |
2519 | - key []string |
2520 | - prefix bool |
2521 | - result int64 |
2522 | - }{ |
2523 | - {[]string{"a", "b", "c"}, false, 10}, |
2524 | - {[]string{"a", "b"}, false, 7}, |
2525 | - {[]string{"a", "z", "b"}, false, 3}, |
2526 | - {[]string{"a", "b", "c"}, true, 10}, |
2527 | - {[]string{"a", "b"}, true, 17}, |
2528 | - {[]string{"a"}, true, 20}, |
2529 | - {[]string{"b"}, true, 0}, |
2530 | - } |
2531 | - |
2532 | - for _, t := range tests { |
2533 | - c.Logf("Test: %#v\n", t) |
2534 | - sum, err := s.store.SumCounter(t.key, t.prefix) |
2535 | - c.Assert(err, IsNil) |
2536 | - c.Assert(sum, Equals, t.result) |
2537 | - } |
2538 | - |
2539 | - // High-level interface works. Now check that the data is |
2540 | - // stored correctly. |
2541 | - counters := s.Session.DB("juju").C("stat.counters") |
2542 | - docs1, err := counters.Count() |
2543 | - c.Assert(err, IsNil) |
2544 | - if docs1 != 3 && docs1 != 4 { |
2545 | - fmt.Errorf("Expected 3 or 4 docs in counters collection, got %d", docs1) |
2546 | - } |
2547 | - |
2548 | - // Hack times so that the next operation adds another document. |
2549 | - err = counters.Update(nil, bson.D{{"$set", bson.D{{"t", 1}}}}) |
2550 | - c.Check(err, IsNil) |
2551 | - |
2552 | - err = s.store.IncCounter([]string{"a", "b", "c"}) |
2553 | - c.Assert(err, IsNil) |
2554 | - |
2555 | - docs2, err := counters.Count() |
2556 | - c.Assert(err, IsNil) |
2557 | - c.Assert(docs2, Equals, docs1+1) |
2558 | - |
2559 | - sum, err = s.store.SumCounter([]string{"a", "b", "c"}, false) |
2560 | - c.Assert(err, IsNil) |
2561 | - c.Assert(sum, Equals, int64(11)) |
2562 | - |
2563 | - sum, err = s.store.SumCounter([]string{"a"}, true) |
2564 | - c.Assert(err, IsNil) |
2565 | - c.Assert(sum, Equals, int64(21)) |
2566 | -} |
2567 | - |
2568 | -func (s *StoreSuite) TestCountersReadOnlySum(c *C) { |
2569 | - // Summing up an unknown key shouldn't add the key to the database. |
2570 | - sum, err := s.store.SumCounter([]string{"a", "b", "c"}, false) |
2571 | - c.Assert(err, IsNil) |
2572 | - c.Assert(sum, Equals, int64(0)) |
2573 | - |
2574 | - tokens := s.Session.DB("juju").C("stat.tokens") |
2575 | - n, err := tokens.Count() |
2576 | - c.Assert(err, IsNil) |
2577 | - c.Assert(n, Equals, 0) |
2578 | -} |
2579 | - |
2580 | -func (s *StoreSuite) TestCountersTokenCaching(c *C) { |
2581 | - sum, err := s.store.SumCounter([]string{"a"}, false) |
2582 | - c.Assert(err, IsNil) |
2583 | - c.Assert(sum, Equals, int64(0)) |
2584 | - |
2585 | - const genSize = 512 |
2586 | - |
2587 | - // All of these will be cached, as we have two generations |
2588 | - // of genSize entries each. |
2589 | - for i := 0; i < genSize*2; i++ { |
2590 | - err := s.store.IncCounter([]string{strconv.Itoa(i)}) |
2591 | - c.Assert(err, IsNil) |
2592 | - } |
2593 | - |
2594 | - // Now go behind the scenes and corrupt all the tokens. |
2595 | - tokens := s.Session.DB("juju").C("stat.tokens") |
2596 | - iter := tokens.Find(nil).Iter() |
2597 | - var t struct { |
2598 | - Id int "_id" |
2599 | - Token string "t" |
2600 | - } |
2601 | - for iter.Next(&t) { |
2602 | - err := tokens.UpdateId(t.Id, bson.M{"$set": bson.M{"t": "corrupted" + t.Token}}) |
2603 | - c.Assert(err, IsNil) |
2604 | - } |
2605 | - c.Assert(iter.Err(), IsNil) |
2606 | - |
2607 | - // We can consult the counters for the cached entries still. |
2608 | - // First, check that the newest generation is good. |
2609 | - for i := genSize; i < genSize*2; i++ { |
2610 | - n, err := s.store.SumCounter([]string{strconv.Itoa(i)}, false) |
2611 | - c.Assert(err, IsNil) |
2612 | - c.Assert(n, Equals, int64(1)) |
2613 | - } |
2614 | - |
2615 | - // Now, we can still access a single entry of the older generation, |
2616 | - // but this will cause the generations to flip and thus the rest |
2617 | - // of the old generation will go away as the top half of the |
2618 | - // entries is turned into the old generation. |
2619 | - n, err := s.store.SumCounter([]string{"0"}, false) |
2620 | - c.Assert(err, IsNil) |
2621 | - c.Assert(n, Equals, int64(1)) |
2622 | - |
2623 | - // Now we've lost access to the rest of the old generation. |
2624 | - for i := 1; i < genSize; i++ { |
2625 | - n, err := s.store.SumCounter([]string{strconv.Itoa(i)}, false) |
2626 | - c.Assert(err, IsNil) |
2627 | - c.Assert(n, Equals, int64(0)) |
2628 | - } |
2629 | - |
2630 | - // But we still have all of the top half available since it was |
2631 | - // moved into the old generation. |
2632 | - for i := genSize; i < genSize*2; i++ { |
2633 | - n, err := s.store.SumCounter([]string{strconv.Itoa(i)}, false) |
2634 | - c.Assert(err, IsNil) |
2635 | - c.Assert(n, Equals, int64(1)) |
2636 | - } |
2637 | -} |
2638 | - |
2639 | -func (s *StoreSuite) TestCounterTokenUniqueness(c *C) { |
2640 | - var wg0, wg1 sync.WaitGroup |
2641 | - wg0.Add(10) |
2642 | - wg1.Add(10) |
2643 | - for i := 0; i < 10; i++ { |
2644 | - go func() { |
2645 | - wg0.Done() |
2646 | - wg0.Wait() |
2647 | - defer wg1.Done() |
2648 | - err := s.store.IncCounter([]string{"a"}) |
2649 | - c.Check(err, IsNil) |
2650 | - }() |
2651 | - } |
2652 | - wg1.Wait() |
2653 | - |
2654 | - sum, err := s.store.SumCounter([]string{"a"}, false) |
2655 | - c.Assert(err, IsNil) |
2656 | - c.Assert(sum, Equals, int64(10)) |
2657 | -} |
2658 | - |
2659 | -func (s *TrivialSuite) TestEventString(c *C) { |
2660 | - c.Assert(store.EventPublished, Matches, "published") |
2661 | - c.Assert(store.EventPublishError, Matches, "publish-error") |
2662 | - for kind := store.CharmEventKind(1); kind < store.EventKindCount; kind++ { |
2663 | - // This guarantees the switch in String is properly |
2664 | - // updated with new event kinds. |
2665 | - c.Assert(kind.String(), Matches, "[a-z-]+") |
2666 | - } |
2667 | -} |
Reviewers: mp+142564_ code.launchpad. net,
Message:
Please take a look.
Description:
Break out store and cmd/charm* into lp:juju-store
Red Squad are going to be working on the charm store, and it was
suggested that an early task would be to split the charm store out
into a separate project. That work has already been done - see
lp:juju-store - and this is the clean-up job.
mgz helped me a lot in doing both these tasks.
Fwiw, juju-store has not been advertised, so feel free to suggest a
different name.
https:/ /code.launchpad .net/~allenap/ juju-core/ break-out- juju-store/ +merge/ 142564
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/7058063/
Affected files: config. yaml config. yaml main.go test.go test.go
A [revision details]
D cmd/charmd/
D cmd/charmd/main.go
D cmd/charmload/
D cmd/charmload/
D store/branch.go
D store/branch_
D store/lpad.go
D store/lpad_test.go
D store/mgo_test.go
D store/server.go
D store/server_
D store/store.go
D store/store_test.go