Merge lp:~themue/juju-core/012-local-storage into lp:~juju/juju-core/trunk

Proposed by Frank Mueller
Status: Merged
Merged at revision: 923
Proposed branch: lp:~themue/juju-core/012-local-storage
Merge into: lp:~juju/juju-core/trunk
Prerequisite: lp:~themue/juju-core/011-local-storage-backend
Diff against target: 278 lines (+256/-0)
3 files modified
environs/local/export_test.go (+6/-0)
environs/local/storage.go (+123/-0)
environs/local/storage_test.go (+127/-0)
To merge this branch: bzr merge lp:~themue/juju-core/012-local-storage
Reviewer Review Type Date Requested Status
The Go Language Gophers Pending
Review via email: mp+149246@code.launchpad.net

Description of the change

local: add storage

Added environ.Storage implementation using the local
storage backend. The tests so far are a copy from
environs/jujutest to check if the behavior is adequate.

https://codereview.appspot.com/7346052/

To post a comment you must log in.
Revision history for this message
Dimiter Naydenov (dimitern) wrote :

Overall LGTM, modulo some trivial suggestions.

https://codereview.appspot.com/7346052/diff/1/environs/local/storage.go
File environs/local/storage.go (right):

https://codereview.appspot.com/7346052/diff/1/environs/local/storage.go#newcode11
environs/local/storage.go:11:
d

https://codereview.appspot.com/7346052/diff/1/environs/local/storage.go#newcode14
environs/local/storage.go:14:
var _ environ.Storage = (*storage)(nil) ?

https://codereview.appspot.com/7346052/diff/1/environs/local/storage.go#newcode30
environs/local/storage.go:30: // exist, it should return a
*NotFoundError.
s/*NotFoundError/NotFoundError - or even use roger's NotFoundf() ?

https://codereview.appspot.com/7346052/diff/1/environs/local/storage.go#newcode68
environs/local/storage.go:68: if string(body) == "" {
len(body) == 0 ?

https://codereview.appspot.com/7346052/diff/1/environs/local/storage.go#newcode76
environs/local/storage.go:76: // URL returns a URL that can be used to
access the given storage file.
s/a URL/an URL

https://codereview.appspot.com/7346052/diff/1/environs/local/storage.go#newcode82
environs/local/storage.go:82: // The length must give the total length
of the file.
s/must give/must be set to/

https://codereview.appspot.com/7346052/diff/1/environs/local/storage.go#newcode98
environs/local/storage.go:98: return errors.New(resp.Status)
Include the StatusCode in the error text?

https://codereview.appspot.com/7346052/diff/1/environs/local/storage.go#newcode120
environs/local/storage.go:120: return errors.New(resp.Status)
as above

https://codereview.appspot.com/7346052/diff/1/environs/local/storage_test.go
File environs/local/storage_test.go (right):

https://codereview.appspot.com/7346052/diff/1/environs/local/storage_test.go#newcode70
environs/local/storage_test.go:70: c.Assert(err, IsNil)
if it's check*, use c.Check, rather than c.Assert - like you're doing
above. Otherwise, call it assert*

https://codereview.appspot.com/7346052/diff/1/environs/local/storage_test.go#newcode83
environs/local/storage_test.go:83: for a := attempt.Start(); a.Next(); {
If you're not retrying (attempt strategy is zeroed out), why do you do
that here?

https://codereview.appspot.com/7346052/diff/1/environs/local/storage_test.go#newcode121
environs/local/storage_test.go:121: c.Check(data, DeepEquals, contents)
Mixing Assert and Check seems a bit random here, care to explain or
unify these?

https://codereview.appspot.com/7346052/

Revision history for this message
Dimiter Naydenov (dimitern) wrote :
Revision history for this message
William Reade (fwereade) wrote :

LGTM with the AttemptStrategy stuff dropped.

https://codereview.appspot.com/7346052/diff/12001/environs/local/storage_test.go
File environs/local/storage_test.go (right):

https://codereview.appspot.com/7346052/diff/12001/environs/local/storage_test.go#newcode18
environs/local/storage_test.go:18: var noRetry =
trivial.AttemptStrategy{}
There's no need for an AttemptStrategy here.

https://codereview.appspot.com/7346052/diff/12001/environs/local/storage_test.go#newcode22
environs/local/storage_test.go:22: // be removed again.
These tests should be written properly, and should not be earmarked for
deletion. Do we really write these sorts of tests against an *Environ*
in the other cases? If so, what have we been smoking?

https://codereview.appspot.com/7346052/diff/12001/environs/local/storage_test.go#newcode85
environs/local/storage_test.go:85: func checkFileDoesNotExist(c *C,
storage environs.StorageReader, name string, attempt
trivial.AttemptStrategy) {
AttemptStrategy not needed.

https://codereview.appspot.com/7346052/diff/12001/environs/local/storage_test.go#newcode99
environs/local/storage_test.go:99: func checkFileHasContents(c *C,
storage environs.StorageReader, name string, contents []byte, attempt
trivial.AttemptStrategy) {
AttemptStrategy not needed.

https://codereview.appspot.com/7346052/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'environs/local/export_test.go'
--- environs/local/export_test.go 2013-02-20 15:00:28 +0000
+++ environs/local/export_test.go 2013-02-20 15:00:28 +0000
@@ -2,8 +2,14 @@
22
3import (3import (
4 "net"4 "net"
5
6 "launchpad.net/juju-core/environs"
5)7)
68
7func Listen(basepath, environName, ip string, port int) (net.Listener, error) {9func Listen(basepath, environName, ip string, port int) (net.Listener, error) {
8 return listen(basepath, environName, ip, port)10 return listen(basepath, environName, ip, port)
9}11}
12
13func NewStorage(address string, port int, environName string) environs.Storage {
14 return newStorage(address, port, environName)
15}
1016
=== added file 'environs/local/storage.go'
--- environs/local/storage.go 1970-01-01 00:00:00 +0000
+++ environs/local/storage.go 2013-02-20 15:00:28 +0000
@@ -0,0 +1,123 @@
1package local
2
3import (
4 "fmt"
5 "io"
6 "io/ioutil"
7 "launchpad.net/juju-core/environs"
8 "net/http"
9 "sort"
10 "strings"
11)
12
13// storage implements the environs.Storage interface.
14type storage struct {
15 baseURL string
16}
17
18var _ environs.Storage = (*storage)(nil)
19
20// newStorage returns a new local storage.
21func newStorage(address string, port int, environName string) *storage {
22 return &storage{
23 baseURL: fmt.Sprintf("http://%s:%d", address, port),
24 }
25}
26
27// Get opens the given storage file and returns a ReadCloser
28// that can be used to read its contents. It is the caller's
29// responsibility to close it after use. If the name does not
30// exist, it should return a *NotFoundError.
31func (s *storage) Get(name string) (io.ReadCloser, error) {
32 url, err := s.URL(name)
33 if err != nil {
34 return nil, err
35 }
36 resp, err := http.Get(url)
37 if err != nil {
38 return nil, err
39 }
40 if resp.StatusCode != 200 {
41 return nil, &environs.NotFoundError{fmt.Errorf("file %q not found", name)}
42 }
43 return resp.Body, nil
44}
45
46// List lists all names in the storage with the given prefix, in
47// alphabetical order. The names in the storage are considered
48// to be in a flat namespace, so the prefix may include slashes
49// and the names returned are the full names for the matching
50// entries.
51func (s *storage) List(prefix string) ([]string, error) {
52 url, err := s.URL(prefix)
53 if err != nil {
54 return nil, err
55 }
56 resp, err := http.Get(url + "*")
57 if err != nil {
58 return nil, err
59 }
60 if resp.StatusCode != 200 {
61 return nil, fmt.Errorf("%d %s", resp.StatusCode, resp.Status)
62 }
63 defer resp.Body.Close()
64 body, err := ioutil.ReadAll(resp.Body)
65 if err != nil {
66 return nil, err
67 }
68 if len(body) == 0 {
69 return nil, nil
70 }
71 names := strings.Split(string(body), "\n")
72 sort.Strings(names)
73 return names, nil
74}
75
76// URL returns an URL that can be used to access the given storage file.
77func (s *storage) URL(name string) (string, error) {
78 return fmt.Sprintf("%s/%s", s.baseURL, name), nil
79}
80
81// Put reads from r and writes to the given storage file.
82// The length must be set to the total length of the file.
83func (s *storage) Put(name string, r io.Reader, length int64) error {
84 url, err := s.URL(name)
85 if err != nil {
86 return err
87 }
88 req, err := http.NewRequest("PUT", url, r)
89 if err != nil {
90 return err
91 }
92 req.Header.Set("Content-Type", "application/octet-stream")
93 resp, err := http.DefaultClient.Do(req)
94 if err != nil {
95 return err
96 }
97 if resp.StatusCode != 201 {
98 return fmt.Errorf("%d %s", resp.StatusCode, resp.Status)
99 }
100 return nil
101}
102
103// Remove removes the given file from the environment's
104// storage. It should not return an error if the file does
105// not exist.
106func (s *storage) Remove(name string) error {
107 url, err := s.URL(name)
108 if err != nil {
109 return err
110 }
111 req, err := http.NewRequest("DELETE", url, nil)
112 if err != nil {
113 return err
114 }
115 resp, err := http.DefaultClient.Do(req)
116 if err != nil {
117 return err
118 }
119 if resp.StatusCode != 200 {
120 return fmt.Errorf("%d %s", resp.StatusCode, resp.Status)
121 }
122 return nil
123}
0124
=== added file 'environs/local/storage_test.go'
--- environs/local/storage_test.go 1970-01-01 00:00:00 +0000
+++ environs/local/storage_test.go 2013-02-20 15:00:28 +0000
@@ -0,0 +1,127 @@
1package local_test
2
3import (
4 "bytes"
5 "io"
6 "io/ioutil"
7 . "launchpad.net/gocheck"
8 "launchpad.net/juju-core/environs"
9 "launchpad.net/juju-core/environs/local"
10 "launchpad.net/juju-core/trivial"
11 "net/http"
12)
13
14type storageSuite struct{}
15
16var _ = Suite(&storageSuite{})
17
18var noRetry = trivial.AttemptStrategy{}
19
20// TestPersistence and the helpers are borrowed from environs/jujutest
21// te check the storage behavior before integration. It will later
22// be removed again.
23func (s *storageSuite) TestPersistence(c *C) {
24 // Non-standard port to avoid conflict with not-yet full
25 // closed listener in backend test.
26 portNo := 60007
27 listener, err := local.Listen(c.MkDir(), environName, "127.0.0.1", portNo)
28 c.Assert(err, IsNil)
29 defer listener.Close()
30 storage := local.NewStorage("127.0.0.1", portNo, environName)
31
32 names := []string{
33 "aa",
34 "zzz/aa",
35 "zzz/bb",
36 }
37 for _, name := range names {
38 checkFileDoesNotExist(c, storage, name, noRetry)
39 checkPutFile(c, storage, name, []byte(name))
40 }
41 checkList(c, storage, "", names)
42 checkList(c, storage, "a", []string{"aa"})
43 checkList(c, storage, "zzz/", []string{"zzz/aa", "zzz/bb"})
44
45 storage2 := local.NewStorage("127.0.0.1", portNo, environName)
46 for _, name := range names {
47 checkFileHasContents(c, storage2, name, []byte(name), noRetry)
48 }
49
50 // remove the first file and check that the others remain.
51 err = storage2.Remove(names[0])
52 c.Check(err, IsNil)
53
54 // check that it's ok to remove a file twice.
55 err = storage2.Remove(names[0])
56 c.Check(err, IsNil)
57
58 // ... and check it's been removed in the other environment
59 checkFileDoesNotExist(c, storage, names[0], noRetry)
60
61 // ... and that the rest of the files are still around
62 checkList(c, storage2, "", names[1:])
63
64 for _, name := range names[1:] {
65 err := storage2.Remove(name)
66 c.Assert(err, IsNil)
67 }
68
69 // check they've all gone
70 checkList(c, storage2, "", nil)
71}
72
73func checkList(c *C, storage environs.StorageReader, prefix string, names []string) {
74 lnames, err := storage.List(prefix)
75 c.Assert(err, IsNil)
76 c.Assert(lnames, DeepEquals, names)
77}
78
79func checkPutFile(c *C, storage environs.StorageWriter, name string, contents []byte) {
80 c.Logf("check putting file %s ...", name)
81 err := storage.Put(name, bytes.NewBuffer(contents), int64(len(contents)))
82 c.Assert(err, IsNil)
83}
84
85func checkFileDoesNotExist(c *C, storage environs.StorageReader, name string, attempt trivial.AttemptStrategy) {
86 var r io.ReadCloser
87 var err error
88 for a := attempt.Start(); a.Next(); {
89 r, err = storage.Get(name)
90 if err != nil {
91 break
92 }
93 }
94 c.Assert(r, IsNil)
95 var notFoundError *environs.NotFoundError
96 c.Assert(err, FitsTypeOf, notFoundError)
97}
98
99func checkFileHasContents(c *C, storage environs.StorageReader, name string, contents []byte, attempt trivial.AttemptStrategy) {
100 r, err := storage.Get(name)
101 c.Assert(err, IsNil)
102 c.Check(r, NotNil)
103 defer r.Close()
104
105 data, err := ioutil.ReadAll(r)
106 c.Check(err, IsNil)
107 c.Check(data, DeepEquals, contents)
108
109 url, err := storage.URL(name)
110 c.Assert(err, IsNil)
111
112 var resp *http.Response
113 for a := attempt.Start(); a.Next(); {
114 resp, err = http.Get(url)
115 c.Assert(err, IsNil)
116 if resp.StatusCode != 404 {
117 break
118 }
119 c.Logf("get retrying after earlier get succeeded. *sigh*.")
120 }
121 c.Assert(err, IsNil)
122 data, err = ioutil.ReadAll(resp.Body)
123 c.Assert(err, IsNil)
124 defer resp.Body.Close()
125 c.Assert(resp.StatusCode, Equals, 200, Commentf("error response: %s", data))
126 c.Check(data, DeepEquals, contents)
127}

Subscribers

People subscribed via source and target branches