Merge lp:~thumper/juju-core/find-jujud into lp:~go-bot/juju-core/trunk

Proposed by Tim Penhey
Status: Merged
Approved by: Tim Penhey
Approved revision: no longer in the source branch.
Merged at revision: 1471
Proposed branch: lp:~thumper/juju-core/find-jujud
Merge into: lp:~go-bot/juju-core/trunk
Prerequisite: lp:~thumper/juju-core/local-provider-bootstrap
Diff against target: 394 lines (+234/-24)
6 files modified
environs/localstorage/storage.go (+7/-1)
environs/localstorage/storage_test.go (+18/-1)
environs/tools/build.go (+106/-15)
environs/tools/build_test.go (+93/-0)
environs/tools/export_test.go (+2/-0)
environs/tools/storage.go (+8/-7)
To merge this branch: bzr merge lp:~thumper/juju-core/find-jujud
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+174918@code.launchpad.net

Commit message

Use pre-built jujud if found.

Instead of rebuilding jujud everytime we want to upload some tools, try to
find jujud next to the juju being executed.

https://codereview.appspot.com/11326043/

Description of the change

Use pre-built jujud if found.

Instead of rebuilding jujud everytime we want to upload some tools, try to
find jujud next to the juju being executed.

When we are uploading tools, we are running the juju command. The
juju executable is normally (when we build it and in the package
version) next to the jujud executable. So, what we do is start with
os.Args[0] and try to find the full path for it. There are three
situations here:
 1. arg[0] is an absolute path, if so, return it
 2. arg[0] is a relative path, if so, join it to the current working
   directory, clean that and return it
 3. arg[0] is just the command, in which case, walk the path looking
   for an executable file that matches.

Amazingly, there is no os.CopyFile, so you have to mangle it yourself.

https://codereview.appspot.com/11326043/

To post a comment you must log in.
Revision history for this message
Tim Penhey (thumper) wrote :

Reviewers: mp+174918_code.launchpad.net,

Message:
Please take a look.

Description:
Use pre-built jujud if found.

Instead of rebuilding jujud everytime we want to upload some tools, try
to
find jujud next to the juju being executed.

When we are uploading tools, we are running the juju command. The
juju executable is normally (when we build it and in the package
version) next to the jujud executable. So, what we do is start with
os.Args[0] and try to find the full path for it. There are three
situations here:
  1. arg[0] is an absolute path, if so, return it
  2. arg[0] is a relative path, if so, join it to the current working
    directory, clean that and return it
  3. arg[0] is just the command, in which case, walk the path looking
    for an executable file that matches.

Amazingly, there is no os.CopyFile, so you have to mangle it yourself.

https://code.launchpad.net/~thumper/juju-core/find-jujud/+merge/174918

Requires:
https://code.launchpad.net/~thumper/juju-core/local-provider-bootstrap/+merge/174913

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/11326043/

Affected files:
   A [revision details]
   M environs/localstorage/storage.go
   M environs/tools/build.go
   M environs/tools/storage.go

Revision history for this message
Martin Packman (gz) wrote :

LGTM

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

https://codereview.appspot.com/11326043/diff/1/environs/localstorage/storage.go#newcode95
environs/localstorage/storage.go:95: // struct so the Close method is
not exposed.
Unrelated drive-by? Kind of thing I'd like a failing test for.

https://codereview.appspot.com/11326043/diff/1/environs/tools/build.go
File environs/tools/build.go (right):

https://codereview.appspot.com/11326043/diff/1/environs/tools/build.go#newcode112
environs/tools/build.go:112: func findExecutable(execFile string)
(string, error) {
Would like some tests for this function.

https://codereview.appspot.com/11326043/diff/1/environs/tools/build.go#newcode131
environs/tools/build.go:131: if info.Mode()&0111 != 0 {
I like spaces even around bitops.

https://codereview.appspot.com/11326043/diff/1/environs/tools/build.go#newcode161
environs/tools/build.go:161: // copy the file into the dir.
I'd factor this out to a seperate function. Just 'cos go doesn't include
it doesn't mean we can't reuse it.

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

https://codereview.appspot.com/11326043/diff/1/environs/tools/storage.go#newcode49
environs/tools/storage.go:49: logger.Debugf("reading v%d.* tools",
majorVersion)
Having logger declared in build.go and used without import in storage.go
is slightly confusing. I will get my head around the go package model at
some point.

https://codereview.appspot.com/11326043/

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

LGTM with mgz's comments taken into account

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

https://codereview.appspot.com/11326043/diff/1/environs/localstorage/storage.go#newcode95
environs/localstorage/storage.go:95: // struct so the Close method is
not exposed.
On 2013/07/16 09:43:00, gz wrote:
> Unrelated drive-by? Kind of thing I'd like a failing test for.

+1

https://codereview.appspot.com/11326043/diff/1/environs/tools/build.go
File environs/tools/build.go (right):

https://codereview.appspot.com/11326043/diff/1/environs/tools/build.go#newcode131
environs/tools/build.go:131: if info.Mode()&0111 != 0 {
On 2013/07/16 09:43:00, gz wrote:
> I like spaces even around bitops.

I think go fmt decides that for you.

https://codereview.appspot.com/11326043/

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

https://codereview.appspot.com/11326043/diff/1/environs/tools/build.go
File environs/tools/build.go (right):

https://codereview.appspot.com/11326043/diff/1/environs/tools/build.go#newcode212
environs/tools/build.go:212: if err := copyExistingJujud(dir); err !=
nil {
Actually, this has percolated a bit and I'm not so happy. This is quite
a big and magical behaviour change... I have on a few occasions
needed/wanted to upload tools that *don't* match the juju binary I'm
using, and this kinda breaks that. Can still be worked around; and
should probably only affect developers; but I'm starting to think that
it's wrong to piggyback on the upload-tools mechanism for this use case.

I should be around tonight to talk about it a little... my big concern
is that we're changing behavior that *will* affect all the users who are
using upload-tools. Maybe I can be convinced that it's only a little,
and maybe even a beneficial, change; but let's discuss tonight.

https://codereview.appspot.com/11326043/

Revision history for this message
Tim Penhey (thumper) wrote :

Working on writing extra tests now.

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

https://codereview.appspot.com/11326043/diff/1/environs/localstorage/storage.go#newcode95
environs/localstorage/storage.go:95: // struct so the Close method is
not exposed.
On 2013/07/16 11:09:50, fwereade wrote:
> On 2013/07/16 09:43:00, gz wrote:
> > Unrelated drive-by? Kind of thing I'd like a failing test for.

> +1

Was needed as part of this branch as Putting from a file into storage,
which has a Close method, caused the Seek(0,0) as the localstorage used
http.NewRequest, which had the side effect of closing the file.

I've added a test now to confirm that Close isn't called.

https://codereview.appspot.com/11326043/diff/1/environs/tools/build.go
File environs/tools/build.go (right):

https://codereview.appspot.com/11326043/diff/1/environs/tools/build.go#newcode131
environs/tools/build.go:131: if info.Mode()&0111 != 0 {
On 2013/07/16 11:09:50, fwereade wrote:
> On 2013/07/16 09:43:00, gz wrote:
> > I like spaces even around bitops.

> I think go fmt decides that for you.

I like spaces too, but go fmt doesn't.

https://codereview.appspot.com/11326043/diff/1/environs/tools/build.go#newcode212
environs/tools/build.go:212: if err := copyExistingJujud(dir); err !=
nil {
On 2013/07/16 11:21:33, fwereade wrote:
> Actually, this has percolated a bit and I'm not so happy. This is
quite a big
> and magical behaviour change... I have on a few occasions
needed/wanted to
> upload tools that *don't* match the juju binary I'm using, and this
kinda breaks
> that. Can still be worked around; and should probably only affect
developers;
> but I'm starting to think that it's wrong to piggyback on the
upload-tools
> mechanism for this use case.

> I should be around tonight to talk about it a little... my big concern
is that
> we're changing behavior that *will* affect all the users who are using
> upload-tools. Maybe I can be convinced that it's only a little, and
maybe even a
> beneficial, change; but let's discuss tonight.

As discussed, we'll go with this for now.

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

https://codereview.appspot.com/11326043/diff/1/environs/tools/storage.go#newcode49
environs/tools/storage.go:49: logger.Debugf("reading v%d.* tools",
majorVersion)
On 2013/07/16 09:43:00, gz wrote:
> Having logger declared in build.go and used without import in
storage.go is
> slightly confusing. I will get my head around the go package model at
some
> point.

Yeah. I'd really prefer the ability to have file local variables, but
go doesn't do that.

https://codereview.appspot.com/11326043/

Revision history for this message
Tim Penhey (thumper) wrote :

Please take a look.

https://codereview.appspot.com/11326043/diff/1/environs/tools/build.go
File environs/tools/build.go (right):

https://codereview.appspot.com/11326043/diff/1/environs/tools/build.go#newcode112
environs/tools/build.go:112: func findExecutable(execFile string)
(string, error) {
On 2013/07/16 09:43:00, gz wrote:
> Would like some tests for this function.

Done.

https://codereview.appspot.com/11326043/diff/1/environs/tools/build.go#newcode161
environs/tools/build.go:161: // copy the file into the dir.
On 2013/07/16 09:43:00, gz wrote:
> I'd factor this out to a seperate function. Just 'cos go doesn't
include it
> doesn't mean we can't reuse it.

I'll add a TODO.

https://codereview.appspot.com/11326043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'environs/localstorage/storage.go'
2--- environs/localstorage/storage.go 2013-07-10 05:07:08 +0000
3+++ environs/localstorage/storage.go 2013-07-17 01:41:27 +0000
4@@ -88,7 +88,13 @@
5 if err != nil {
6 return err
7 }
8- req, err := http.NewRequest("PUT", url, r)
9+ // Here we wrap up the reader. For some freaky unexplainable reason, the
10+ // http library will call Close on the reader if it has a Close method
11+ // available. Since we sometimes reuse the reader, especially when
12+ // putting tools, we don't want Close called. So we wrap the reader in a
13+ // struct so the Close method is not exposed.
14+ justReader := struct{ io.Reader }{r}
15+ req, err := http.NewRequest("PUT", url, justReader)
16 if err != nil {
17 return err
18 }
19
20=== modified file 'environs/localstorage/storage_test.go'
21--- environs/localstorage/storage_test.go 2013-07-10 05:07:08 +0000
22+++ environs/localstorage/storage_test.go 2013-07-17 01:41:27 +0000
23@@ -6,6 +6,7 @@
24 import (
25 "bytes"
26 "fmt"
27+ "io"
28 "io/ioutil"
29 "net/http"
30
31@@ -14,6 +15,7 @@
32 "launchpad.net/juju-core/environs"
33 "launchpad.net/juju-core/environs/localstorage"
34 "launchpad.net/juju-core/errors"
35+ jc "launchpad.net/juju-core/testing/checkers"
36 )
37
38 type storageSuite struct{}
39@@ -77,10 +79,25 @@
40 c.Assert(lnames, DeepEquals, names)
41 }
42
43+type readerWithClose struct {
44+ *bytes.Buffer
45+ closeCalled bool
46+}
47+
48+var _ io.Reader = (*readerWithClose)(nil)
49+var _ io.Closer = (*readerWithClose)(nil)
50+
51+func (r *readerWithClose) Close() error {
52+ r.closeCalled = true
53+ return nil
54+}
55+
56 func checkPutFile(c *C, storage environs.StorageWriter, name string, contents []byte) {
57 c.Logf("check putting file %s ...", name)
58- err := storage.Put(name, bytes.NewBuffer(contents), int64(len(contents)))
59+ reader := &readerWithClose{bytes.NewBuffer(contents), false}
60+ err := storage.Put(name, reader, int64(len(contents)))
61 c.Assert(err, IsNil)
62+ c.Assert(reader.closeCalled, jc.IsFalse)
63 }
64
65 func checkFileDoesNotExist(c *C, storage environs.StorageReader, name string) {
66
67=== modified file 'environs/tools/build.go'
68--- environs/tools/build.go 2013-05-02 15:55:42 +0000
69+++ environs/tools/build.go 2013-07-17 01:41:27 +0000
70@@ -9,13 +9,18 @@
71 "fmt"
72 "io"
73 "io/ioutil"
74- "launchpad.net/juju-core/version"
75 "os"
76 "os/exec"
77 "path/filepath"
78 "strings"
79+
80+ "launchpad.net/loggo"
81+
82+ "launchpad.net/juju-core/version"
83 )
84
85+var logger = loggo.GetLogger("juju.environs.tools")
86+
87 // archive writes the executable files found in the given directory in
88 // gzipped tar format to w. An error is returned if an entry inside dir
89 // is not a regular executable file.
90@@ -33,6 +38,7 @@
91
92 for _, ent := range entries {
93 h := tarHeader(ent)
94+ logger.Debugf("adding entry: %#v", h)
95 // ignore local umask
96 if isExecutable(ent) {
97 h.Mode = 0755
98@@ -103,17 +109,80 @@
99 return append(env, val)
100 }
101
102-// bundleTools bundles all the current juju tools in gzipped tar
103-// format to the given writer.
104-// If forceVersion is not nil, a FORCE-VERSION file is included in
105-// the tools bundle so it will lie about its current version number.
106-func bundleTools(w io.Writer, forceVersion *version.Number) (version.Binary, error) {
107- dir, err := ioutil.TempDir("", "juju-tools")
108- if err != nil {
109- return version.Binary{}, err
110- }
111- defer os.RemoveAll(dir)
112-
113+func findExecutable(execFile string) (string, error) {
114+ logger.Debugf("looking for: %s", execFile)
115+ if filepath.IsAbs(execFile) {
116+ return execFile, nil
117+ }
118+
119+ dir, file := filepath.Split(execFile)
120+
121+ // Now we have two possibilities:
122+ // file == path indicating that the PATH was searched
123+ // dir != "" indicating that it is a relative path
124+
125+ if dir == "" {
126+ path := os.Getenv("PATH")
127+ for _, name := range filepath.SplitList(path) {
128+ result := filepath.Join(name, file)
129+ info, err := os.Stat(result)
130+ if err == nil {
131+ // Sanity check to see if executable.
132+ if info.Mode()&0111 != 0 {
133+ return result, nil
134+ }
135+ }
136+ }
137+
138+ return "", fmt.Errorf("could not find %q in the path", file)
139+ }
140+ cwd, err := os.Getwd()
141+ if err != nil {
142+ return "", err
143+ }
144+ return filepath.Clean(filepath.Join(cwd, execFile)), nil
145+}
146+
147+func copyExistingJujud(dir string) error {
148+ // Assume that the user is running juju.
149+ jujuLocation, err := findExecutable(os.Args[0])
150+ if err != nil {
151+ logger.Infof("%v", err)
152+ return err
153+ }
154+ jujudLocation := filepath.Join(filepath.Dir(jujuLocation), "jujud")
155+ logger.Debugf("checking: %s", jujudLocation)
156+ info, err := os.Stat(jujudLocation)
157+ if err != nil {
158+ logger.Infof("couldn't find existing jujud")
159+ return err
160+ }
161+ logger.Infof("found existing jujud")
162+ // TODO(thumper): break this out into a util function.
163+ // copy the file into the dir.
164+ source, err := os.Open(jujudLocation)
165+ if err != nil {
166+ logger.Infof("open source failed: %v", err)
167+ return err
168+ }
169+ defer source.Close()
170+ target := filepath.Join(dir, "jujud")
171+ logger.Infof("target: %v", target)
172+ destination, err := os.OpenFile(target, os.O_RDWR|os.O_TRUNC|os.O_CREATE, info.Mode())
173+ if err != nil {
174+ logger.Infof("open destination failed: %v", err)
175+ return err
176+ }
177+ defer destination.Close()
178+ _, err = io.Copy(destination, source)
179+ if err != nil {
180+ return err
181+ }
182+ return nil
183+}
184+
185+func buildJujud(dir string) error {
186+ logger.Infof("building jujud")
187 cmds := [][]string{
188 {"go", "install", "launchpad.net/juju-core/cmd/jujud"},
189 {"strip", dir + "/jujud"},
190@@ -124,10 +193,32 @@
191 cmd.Env = env
192 out, err := cmd.CombinedOutput()
193 if err != nil {
194- return version.Binary{}, fmt.Errorf("build command %q failed: %v; %s", args[0], err, out)
195- }
196- }
197+ return fmt.Errorf("build command %q failed: %v; %s", args[0], err, out)
198+ }
199+ }
200+ return nil
201+}
202+
203+// bundleTools bundles all the current juju tools in gzipped tar
204+// format to the given writer.
205+// If forceVersion is not nil, a FORCE-VERSION file is included in
206+// the tools bundle so it will lie about its current version number.
207+func bundleTools(w io.Writer, forceVersion *version.Number) (version.Binary, error) {
208+ dir, err := ioutil.TempDir("", "juju-tools")
209+ if err != nil {
210+ return version.Binary{}, err
211+ }
212+ defer os.RemoveAll(dir)
213+
214+ if err := copyExistingJujud(dir); err != nil {
215+ logger.Debugf("copy existing failed: %v", err)
216+ if err := buildJujud(dir); err != nil {
217+ return version.Binary{}, err
218+ }
219+ }
220+
221 if forceVersion != nil {
222+ logger.Debugf("forcing version to %s", forceVersion)
223 if err := ioutil.WriteFile(filepath.Join(dir, "FORCE-VERSION"), []byte(forceVersion.String()), 0666); err != nil {
224 return version.Binary{}, err
225 }
226
227=== added file 'environs/tools/build_test.go'
228--- environs/tools/build_test.go 1970-01-01 00:00:00 +0000
229+++ environs/tools/build_test.go 2013-07-17 01:41:27 +0000
230@@ -0,0 +1,93 @@
231+// Copyright 2013 Canonical Ltd.
232+// Licensed under the AGPLv3, see LICENCE file for details.
233+
234+package tools_test
235+
236+import (
237+ "fmt"
238+ "io/ioutil"
239+ "os"
240+ "path/filepath"
241+
242+ gc "launchpad.net/gocheck"
243+
244+ "launchpad.net/juju-core/environs/tools"
245+ "launchpad.net/juju-core/testing"
246+)
247+
248+type buildSuite struct {
249+ testing.LoggingSuite
250+ restore func()
251+ cwd string
252+ filePath string
253+}
254+
255+var _ = gc.Suite(&buildSuite{})
256+
257+func (b *buildSuite) SetUpTest(c *gc.C) {
258+ b.LoggingSuite.SetUpTest(c)
259+
260+ dir1 := c.MkDir()
261+ dir2 := c.MkDir()
262+
263+ c.Log(dir1)
264+ c.Log(dir2)
265+
266+ path := os.Getenv("PATH")
267+ os.Setenv("PATH", fmt.Sprintf("%s:%s:%s", dir1, dir2, path))
268+
269+ // Make an executable file called "juju-test" in dir2.
270+ b.filePath = filepath.Join(dir2, "juju-test")
271+ err := ioutil.WriteFile(
272+ b.filePath,
273+ []byte("doesn't matter, we don't execute it"),
274+ 0755)
275+ c.Assert(err, gc.IsNil)
276+
277+ cwd, err := os.Getwd()
278+ c.Assert(err, gc.IsNil)
279+
280+ b.cwd = c.MkDir()
281+ err = os.Chdir(b.cwd)
282+ c.Assert(err, gc.IsNil)
283+
284+ b.restore = func() {
285+ os.Setenv("PATH", path)
286+ os.Chdir(cwd)
287+ }
288+}
289+
290+func (b *buildSuite) TearDownTest(c *gc.C) {
291+ b.restore()
292+ b.LoggingSuite.TearDownTest(c)
293+}
294+
295+func (b *buildSuite) TestFindExecutable(c *gc.C) {
296+
297+ for _, test := range []struct {
298+ execFile string
299+ expected string
300+ errorMatch string
301+ }{{
302+ execFile: "/some/absolute/path",
303+ expected: "/some/absolute/path",
304+ }, {
305+ execFile: "./foo",
306+ expected: filepath.Join(b.cwd, "foo"),
307+ }, {
308+ execFile: "juju-test",
309+ expected: b.filePath,
310+ }, {
311+ execFile: "non-existent-exec-file",
312+ errorMatch: `could not find "non-existent-exec-file" in the path`,
313+ }} {
314+ result, err := tools.FindExecutable(test.execFile)
315+ if test.errorMatch == "" {
316+ c.Assert(err, gc.IsNil)
317+ c.Assert(result, gc.Equals, test.expected)
318+ } else {
319+ c.Assert(err, gc.ErrorMatches, test.errorMatch)
320+ c.Assert(result, gc.Equals, "")
321+ }
322+ }
323+}
324
325=== modified file 'environs/tools/export_test.go'
326--- environs/tools/export_test.go 2013-05-02 15:55:42 +0000
327+++ environs/tools/export_test.go 2013-07-17 01:41:27 +0000
328@@ -4,3 +4,5 @@
329 package tools
330
331 var Setenv = setenv
332+
333+var FindExecutable = findExecutable
334
335=== modified file 'environs/tools/storage.go'
336--- environs/tools/storage.go 2013-05-02 15:55:42 +0000
337+++ environs/tools/storage.go 2013-07-17 01:41:27 +0000
338@@ -8,11 +8,11 @@
339 "fmt"
340 "io"
341 "io/ioutil"
342- "launchpad.net/juju-core/log"
343+ "os"
344+ "strings"
345+
346 "launchpad.net/juju-core/state"
347 "launchpad.net/juju-core/version"
348- "os"
349- "strings"
350 )
351
352 var ErrNoTools = errors.New("no tools available")
353@@ -46,7 +46,7 @@
354 // ReadList returns a List of the tools in store with the given major version.
355 // If store contains no such tools, it returns ErrNoMatches.
356 func ReadList(storage URLLister, majorVersion int) (List, error) {
357- log.Debugf("environs/tools: reading v%d.* tools", majorVersion)
358+ logger.Debugf("reading v%d.* tools", majorVersion)
359 names, err := storage.List(toolPrefix)
360 if err != nil {
361 return nil, err
362@@ -66,7 +66,7 @@
363 if t.Major != majorVersion {
364 continue
365 }
366- log.Debugf("environs/tools: found %s", vers)
367+ logger.Debugf("found %s", vers)
368 if t.URL, err = storage.URL(name); err != nil {
369 return nil, err
370 }
371@@ -99,6 +99,7 @@
372 // TODO(rog) find binaries from $PATH when not using a development
373 // version of juju within a $GOPATH.
374
375+ logger.Debugf("Uploading tools for %v", fakeSeries)
376 // We create the entire archive before asking the environment to
377 // start uploading so that we can be sure we have archived
378 // correctly.
379@@ -117,13 +118,13 @@
380 return nil, fmt.Errorf("cannot stat newly made tools archive: %v", err)
381 }
382 size := fileInfo.Size()
383- log.Infof("environs/tools: built %v (%dkB)", toolsVersion, (size+512)/1024)
384+ logger.Infof("built %v (%dkB)", toolsVersion, (size+512)/1024)
385 putTools := func(vers version.Binary) (string, error) {
386 if _, err := f.Seek(0, 0); err != nil {
387 return "", fmt.Errorf("cannot seek to start of tools archive: %v", err)
388 }
389 name := StorageName(vers)
390- log.Infof("environs/tools: uploading %s", vers)
391+ logger.Infof("uploading %s", vers)
392 if err := storage.Put(name, f, size); err != nil {
393 return "", err
394 }

Subscribers

People subscribed via source and target branches

to status/vote changes: