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
1=== modified file 'environs/local/export_test.go'
2--- environs/local/export_test.go 2013-02-20 15:00:28 +0000
3+++ environs/local/export_test.go 2013-02-20 15:00:28 +0000
4@@ -2,8 +2,14 @@
5
6 import (
7 "net"
8+
9+ "launchpad.net/juju-core/environs"
10 )
11
12 func Listen(basepath, environName, ip string, port int) (net.Listener, error) {
13 return listen(basepath, environName, ip, port)
14 }
15+
16+func NewStorage(address string, port int, environName string) environs.Storage {
17+ return newStorage(address, port, environName)
18+}
19
20=== added file 'environs/local/storage.go'
21--- environs/local/storage.go 1970-01-01 00:00:00 +0000
22+++ environs/local/storage.go 2013-02-20 15:00:28 +0000
23@@ -0,0 +1,123 @@
24+package local
25+
26+import (
27+ "fmt"
28+ "io"
29+ "io/ioutil"
30+ "launchpad.net/juju-core/environs"
31+ "net/http"
32+ "sort"
33+ "strings"
34+)
35+
36+// storage implements the environs.Storage interface.
37+type storage struct {
38+ baseURL string
39+}
40+
41+var _ environs.Storage = (*storage)(nil)
42+
43+// newStorage returns a new local storage.
44+func newStorage(address string, port int, environName string) *storage {
45+ return &storage{
46+ baseURL: fmt.Sprintf("http://%s:%d", address, port),
47+ }
48+}
49+
50+// Get opens the given storage file and returns a ReadCloser
51+// that can be used to read its contents. It is the caller's
52+// responsibility to close it after use. If the name does not
53+// exist, it should return a *NotFoundError.
54+func (s *storage) Get(name string) (io.ReadCloser, error) {
55+ url, err := s.URL(name)
56+ if err != nil {
57+ return nil, err
58+ }
59+ resp, err := http.Get(url)
60+ if err != nil {
61+ return nil, err
62+ }
63+ if resp.StatusCode != 200 {
64+ return nil, &environs.NotFoundError{fmt.Errorf("file %q not found", name)}
65+ }
66+ return resp.Body, nil
67+}
68+
69+// List lists all names in the storage with the given prefix, in
70+// alphabetical order. The names in the storage are considered
71+// to be in a flat namespace, so the prefix may include slashes
72+// and the names returned are the full names for the matching
73+// entries.
74+func (s *storage) List(prefix string) ([]string, error) {
75+ url, err := s.URL(prefix)
76+ if err != nil {
77+ return nil, err
78+ }
79+ resp, err := http.Get(url + "*")
80+ if err != nil {
81+ return nil, err
82+ }
83+ if resp.StatusCode != 200 {
84+ return nil, fmt.Errorf("%d %s", resp.StatusCode, resp.Status)
85+ }
86+ defer resp.Body.Close()
87+ body, err := ioutil.ReadAll(resp.Body)
88+ if err != nil {
89+ return nil, err
90+ }
91+ if len(body) == 0 {
92+ return nil, nil
93+ }
94+ names := strings.Split(string(body), "\n")
95+ sort.Strings(names)
96+ return names, nil
97+}
98+
99+// URL returns an URL that can be used to access the given storage file.
100+func (s *storage) URL(name string) (string, error) {
101+ return fmt.Sprintf("%s/%s", s.baseURL, name), nil
102+}
103+
104+// Put reads from r and writes to the given storage file.
105+// The length must be set to the total length of the file.
106+func (s *storage) Put(name string, r io.Reader, length int64) error {
107+ url, err := s.URL(name)
108+ if err != nil {
109+ return err
110+ }
111+ req, err := http.NewRequest("PUT", url, r)
112+ if err != nil {
113+ return err
114+ }
115+ req.Header.Set("Content-Type", "application/octet-stream")
116+ resp, err := http.DefaultClient.Do(req)
117+ if err != nil {
118+ return err
119+ }
120+ if resp.StatusCode != 201 {
121+ return fmt.Errorf("%d %s", resp.StatusCode, resp.Status)
122+ }
123+ return nil
124+}
125+
126+// Remove removes the given file from the environment's
127+// storage. It should not return an error if the file does
128+// not exist.
129+func (s *storage) Remove(name string) error {
130+ url, err := s.URL(name)
131+ if err != nil {
132+ return err
133+ }
134+ req, err := http.NewRequest("DELETE", url, nil)
135+ if err != nil {
136+ return err
137+ }
138+ resp, err := http.DefaultClient.Do(req)
139+ if err != nil {
140+ return err
141+ }
142+ if resp.StatusCode != 200 {
143+ return fmt.Errorf("%d %s", resp.StatusCode, resp.Status)
144+ }
145+ return nil
146+}
147
148=== added file 'environs/local/storage_test.go'
149--- environs/local/storage_test.go 1970-01-01 00:00:00 +0000
150+++ environs/local/storage_test.go 2013-02-20 15:00:28 +0000
151@@ -0,0 +1,127 @@
152+package local_test
153+
154+import (
155+ "bytes"
156+ "io"
157+ "io/ioutil"
158+ . "launchpad.net/gocheck"
159+ "launchpad.net/juju-core/environs"
160+ "launchpad.net/juju-core/environs/local"
161+ "launchpad.net/juju-core/trivial"
162+ "net/http"
163+)
164+
165+type storageSuite struct{}
166+
167+var _ = Suite(&storageSuite{})
168+
169+var noRetry = trivial.AttemptStrategy{}
170+
171+// TestPersistence and the helpers are borrowed from environs/jujutest
172+// te check the storage behavior before integration. It will later
173+// be removed again.
174+func (s *storageSuite) TestPersistence(c *C) {
175+ // Non-standard port to avoid conflict with not-yet full
176+ // closed listener in backend test.
177+ portNo := 60007
178+ listener, err := local.Listen(c.MkDir(), environName, "127.0.0.1", portNo)
179+ c.Assert(err, IsNil)
180+ defer listener.Close()
181+ storage := local.NewStorage("127.0.0.1", portNo, environName)
182+
183+ names := []string{
184+ "aa",
185+ "zzz/aa",
186+ "zzz/bb",
187+ }
188+ for _, name := range names {
189+ checkFileDoesNotExist(c, storage, name, noRetry)
190+ checkPutFile(c, storage, name, []byte(name))
191+ }
192+ checkList(c, storage, "", names)
193+ checkList(c, storage, "a", []string{"aa"})
194+ checkList(c, storage, "zzz/", []string{"zzz/aa", "zzz/bb"})
195+
196+ storage2 := local.NewStorage("127.0.0.1", portNo, environName)
197+ for _, name := range names {
198+ checkFileHasContents(c, storage2, name, []byte(name), noRetry)
199+ }
200+
201+ // remove the first file and check that the others remain.
202+ err = storage2.Remove(names[0])
203+ c.Check(err, IsNil)
204+
205+ // check that it's ok to remove a file twice.
206+ err = storage2.Remove(names[0])
207+ c.Check(err, IsNil)
208+
209+ // ... and check it's been removed in the other environment
210+ checkFileDoesNotExist(c, storage, names[0], noRetry)
211+
212+ // ... and that the rest of the files are still around
213+ checkList(c, storage2, "", names[1:])
214+
215+ for _, name := range names[1:] {
216+ err := storage2.Remove(name)
217+ c.Assert(err, IsNil)
218+ }
219+
220+ // check they've all gone
221+ checkList(c, storage2, "", nil)
222+}
223+
224+func checkList(c *C, storage environs.StorageReader, prefix string, names []string) {
225+ lnames, err := storage.List(prefix)
226+ c.Assert(err, IsNil)
227+ c.Assert(lnames, DeepEquals, names)
228+}
229+
230+func checkPutFile(c *C, storage environs.StorageWriter, name string, contents []byte) {
231+ c.Logf("check putting file %s ...", name)
232+ err := storage.Put(name, bytes.NewBuffer(contents), int64(len(contents)))
233+ c.Assert(err, IsNil)
234+}
235+
236+func checkFileDoesNotExist(c *C, storage environs.StorageReader, name string, attempt trivial.AttemptStrategy) {
237+ var r io.ReadCloser
238+ var err error
239+ for a := attempt.Start(); a.Next(); {
240+ r, err = storage.Get(name)
241+ if err != nil {
242+ break
243+ }
244+ }
245+ c.Assert(r, IsNil)
246+ var notFoundError *environs.NotFoundError
247+ c.Assert(err, FitsTypeOf, notFoundError)
248+}
249+
250+func checkFileHasContents(c *C, storage environs.StorageReader, name string, contents []byte, attempt trivial.AttemptStrategy) {
251+ r, err := storage.Get(name)
252+ c.Assert(err, IsNil)
253+ c.Check(r, NotNil)
254+ defer r.Close()
255+
256+ data, err := ioutil.ReadAll(r)
257+ c.Check(err, IsNil)
258+ c.Check(data, DeepEquals, contents)
259+
260+ url, err := storage.URL(name)
261+ c.Assert(err, IsNil)
262+
263+ var resp *http.Response
264+ for a := attempt.Start(); a.Next(); {
265+ resp, err = http.Get(url)
266+ c.Assert(err, IsNil)
267+ if resp.StatusCode != 404 {
268+ break
269+ }
270+ c.Logf("get retrying after earlier get succeeded. *sigh*.")
271+ }
272+ c.Assert(err, IsNil)
273+ data, err = ioutil.ReadAll(resp.Body)
274+ c.Assert(err, IsNil)
275+ defer resp.Body.Close()
276+ c.Assert(resp.StatusCode, Equals, 200, Commentf("error response: %s", data))
277+ c.Check(data, DeepEquals, contents)
278+}

Subscribers

People subscribed via source and target branches