Browse code

'docker pull' and 'docker put' automatically detect tar compression (gzip, bzip2 or uncompressed). -j and -z flags are no longer required.

Solomon Hykes authored on 2013/02/23 05:28:25
Showing 10 changed files
... ...
@@ -157,12 +157,12 @@ Step by step host setup
157 157
 3. Type the following commands:
158 158
 
159 159
         apt-get update
160
-        apt-get install lxc wget
160
+        apt-get install lxc wget bsdtar
161 161
 
162 162
 4. Download the latest version of the [docker binaries](https://dl.dropbox.com/u/20637798/docker.tar.gz) (`wget https://dl.dropbox.com/u/20637798/docker.tar.gz`) (warning: this may not be the most up-to-date build)
163 163
 5. Extract the contents of the tar file `tar -xf docker.tar.gz`
164 164
 6. Launch the docker daemon `./dockerd`
165
-7. Download a base image by running 'docker pull -j base'
165
+7. Download a base image by running 'docker pull base'
166 166
 
167 167
 
168 168
 Client installation
... ...
@@ -14,7 +14,7 @@ func FakeTar() (io.Reader, error) {
14 14
 	content := []byte("Hello world!\n")
15 15
 	buf := new(bytes.Buffer)
16 16
 	tw := tar.NewWriter(buf)
17
-	for _, name := range []string {"/etc/postgres/postgres.conf", "/etc/passwd", "/var/log/postgres", "/var/log/postgres/postgres.conf"} {
17
+	for _, name := range []string {"hello", "etc/postgres/postgres.conf", "etc/passwd", "var/log/postgres/postgres.conf"} {
18 18
 		hdr := new(tar.Header)
19 19
 		hdr.Size = int64(len(content))
20 20
 		hdr.Name = name
... ...
@@ -10,6 +10,7 @@ import (
10 10
 	"strings"
11 11
 	"syscall"
12 12
 	"time"
13
+	"github.com/dotcloud/docker/image"
13 14
 )
14 15
 
15 16
 type Filesystem struct {
... ...
@@ -104,7 +105,7 @@ func (fs *Filesystem) Tar() (io.Reader, error) {
104 104
 	if err := fs.EnsureMounted(); err != nil {
105 105
 		return nil, err
106 106
 	}
107
-	return Tar(fs.RootFS)
107
+	return image.Tar(fs.RootFS, image.Uncompressed)
108 108
 }
109 109
 
110 110
 func (fs *Filesystem) EnsureMounted() error {
... ...
@@ -61,3 +61,26 @@ func Go(f func() error) chan error {
61 61
 	return ch
62 62
 }
63 63
 
64
+// Pv wraps an io.Reader such that it is passed through unchanged,
65
+// but logs the number of bytes copied (comparable to the unix command pv)
66
+func Pv(src io.Reader, info io.Writer) io.Reader {
67
+	var totalBytes int
68
+	data := make([]byte, 2048)
69
+	r, w := io.Pipe()
70
+	go func() {
71
+		for {
72
+			if n, err := src.Read(data); err != nil {
73
+				w.CloseWithError(err)
74
+				return
75
+			} else {
76
+				totalBytes += n
77
+				fmt.Fprintf(info, "--> %d bytes\n", totalBytes)
78
+				if _, err = w.Write(data[:n]); err != nil {
79
+					return
80
+				}
81
+			}
82
+		}
83
+	}()
84
+	return r
85
+}
86
+
64 87
new file mode 100644
... ...
@@ -0,0 +1,71 @@
0
+package image
1
+
2
+import (
3
+	"io"
4
+	"io/ioutil"
5
+	"os/exec"
6
+	"errors"
7
+)
8
+
9
+type Compression uint32
10
+
11
+const (
12
+	Uncompressed	Compression = iota
13
+	Bzip2
14
+	Gzip
15
+)
16
+
17
+func (compression *Compression) Flag() string {
18
+	switch *compression {
19
+		case Bzip2: return "j"
20
+		case Gzip: return "z"
21
+	}
22
+	return ""
23
+}
24
+
25
+func Tar(path string, compression Compression) (io.Reader, error) {
26
+	cmd := exec.Command("bsdtar", "-f", "-", "-C", path, "-c" + compression.Flag(), ".")
27
+	return CmdStream(cmd)
28
+}
29
+
30
+func Untar(archive io.Reader, path string) error {
31
+	cmd := exec.Command("bsdtar", "-f", "-", "-C", path, "-x")
32
+	cmd.Stdin = archive
33
+	output, err := cmd.CombinedOutput()
34
+	if err != nil {
35
+		return errors.New(err.Error() + ": " + string(output))
36
+	}
37
+	return nil
38
+}
39
+
40
+func CmdStream(cmd *exec.Cmd) (io.Reader, error) {
41
+	stdout, err := cmd.StdoutPipe()
42
+	if err != nil {
43
+		return nil, err
44
+	}
45
+	stderr, err := cmd.StderrPipe()
46
+	if err != nil {
47
+		return nil, err
48
+	}
49
+	pipeR, pipeW := io.Pipe()
50
+	go func() {
51
+		_, err := io.Copy(pipeW, stdout)
52
+		if err != nil {
53
+			pipeW.CloseWithError(err)
54
+		}
55
+		errText, e := ioutil.ReadAll(stderr)
56
+		if e != nil {
57
+			errText = []byte("(...couldn't fetch stderr: " + e.Error() + ")")
58
+		}
59
+		if err := cmd.Wait(); err != nil {
60
+			// FIXME: can this block if stderr outputs more than the size of StderrPipe()'s buffer?
61
+			pipeW.CloseWithError(errors.New(err.Error() + ": " + string(errText)))
62
+		} else {
63
+			pipeW.Close()
64
+		}
65
+	}()
66
+	if err := cmd.Start(); err != nil {
67
+		return nil, err
68
+	}
69
+	return pipeR, nil
70
+}
0 71
new file mode 100644
... ...
@@ -0,0 +1,54 @@
0
+package image
1
+
2
+import (
3
+	"testing"
4
+	"os"
5
+	"os/exec"
6
+	"io/ioutil"
7
+)
8
+
9
+func TestCmdStreamBad(t *testing.T) {
10
+	badCmd := exec.Command("/bin/sh", "-c", "echo hello; echo >&2 error couldn\\'t reverse the phase pulser; exit 1")
11
+	out, err := CmdStream(badCmd)
12
+	if err != nil {
13
+		t.Fatalf("Failed to start command: " + err.Error())
14
+	}
15
+	if output, err := ioutil.ReadAll(out); err == nil {
16
+		t.Fatalf("Command should have failed")
17
+	} else if err.Error() != "exit status 1: error couldn't reverse the phase pulser\n" {
18
+		t.Fatalf("Wrong error value (%s)", err.Error())
19
+	} else if s := string(output); s != "hello\n" {
20
+		t.Fatalf("Command output should be '%s', not '%s'", "hello\\n", output)
21
+	}
22
+}
23
+
24
+func TestCmdStreamGood(t *testing.T) {
25
+	cmd := exec.Command("/bin/sh", "-c", "echo hello; exit 0")
26
+	out, err := CmdStream(cmd)
27
+	if err != nil {
28
+		t.Fatal(err)
29
+	}
30
+	if output, err := ioutil.ReadAll(out); err != nil {
31
+		t.Fatalf("Command should not have failed (err=%s)", err)
32
+	} else if s := string(output); s != "hello\n" {
33
+		t.Fatalf("Command output should be '%s', not '%s'", "hello\\n", output)
34
+	}
35
+}
36
+
37
+func TestTarUntar(t *testing.T) {
38
+	archive, err := Tar(".", Uncompressed)
39
+	if err != nil {
40
+		t.Fatal(err)
41
+	}
42
+	tmp, err := ioutil.TempDir("", "docker-test-untar")
43
+	if err != nil {
44
+		t.Fatal(err)
45
+	}
46
+	defer os.RemoveAll(tmp)
47
+	if err := Untar(archive, tmp); err != nil {
48
+		t.Fatal(err)
49
+	}
50
+	if _, err := os.Stat(tmp); err != nil {
51
+		t.Fatalf("Error stating %s: %s", tmp, err.Error())
52
+	}
53
+}
... ...
@@ -44,16 +44,10 @@ func New(root string) (*Store, error) {
44 44
 	}, nil
45 45
 }
46 46
 
47
-type Compression uint32
48
-
49
-const (
50
-	Uncompressed	Compression = iota
51
-	Bzip2
52
-	Gzip
53
-)
54
-
55
-func (store *Store) Import(name string, archive io.Reader, stderr io.Writer, parent *Image, compression Compression) (*Image, error) {
56
-	layer, err := store.Layers.AddLayer(archive, stderr, compression)
47
+// Import creates a new image from the contents of `archive` and registers it in the store as `name`.
48
+// If `parent` is not nil, it will registered as the parent of the new image.
49
+func (store *Store) Import(name string, archive io.Reader, parent *Image) (*Image, error) {
50
+	layer, err := store.Layers.AddLayer(archive)
57 51
 	if err != nil {
58 52
 		return nil, err
59 53
 	}
... ...
@@ -7,7 +7,6 @@ import (
7 7
 	"io"
8 8
 	"io/ioutil"
9 9
 	"os"
10
-	"os/exec"
11 10
 	"github.com/dotcloud/docker/future"
12 11
 )
13 12
 
... ...
@@ -82,50 +81,42 @@ func (store *LayerStore) layerPath(id string) string {
82 82
 }
83 83
 
84 84
 
85
-func (store *LayerStore) AddLayer(archive io.Reader, stderr io.Writer, compression Compression) (string, error) {
85
+func (store *LayerStore) AddLayer(archive io.Reader) (string, error) {
86
+	errors := make(chan error)
87
+	// Untar
86 88
 	tmp, err := store.Mktemp()
87 89
 	defer os.RemoveAll(tmp)
88 90
 	if err != nil {
89 91
 		return "", err
90 92
 	}
91
-	extractFlags := "-x"
92
-	if compression == Bzip2 {
93
-		extractFlags += "j"
94
-	} else if compression == Gzip {
95
-		extractFlags += "z"
96
-	}
97
-	untarCmd := exec.Command("tar", "-C", tmp, extractFlags)
98
-	untarW, err := untarCmd.StdinPipe()
99
-	if err != nil {
100
-		return "", err
101
-	}
102
-	untarStderr, err := untarCmd.StderrPipe()
103
-	if err != nil {
104
-		return "", err
105
-	}
106
-	go io.Copy(stderr, untarStderr)
107
-	untarStdout, err := untarCmd.StdoutPipe()
108
-	if err != nil {
109
-		return "", err
110
-	}
111
-	go io.Copy(stderr, untarStdout)
112
-	untarCmd.Start()
93
+	untarR, untarW := io.Pipe()
94
+	go func() {
95
+		errors <- Untar(untarR, tmp)
96
+	}()
97
+	// Compute ID
98
+	var id string
113 99
 	hashR, hashW := io.Pipe()
114
-	job_copy := future.Go(func() error {
115
-		_, err := io.Copy(io.MultiWriter(hashW, untarW), archive)
116
-		hashW.Close()
117
-		untarW.Close()
118
-		return err
119
-	})
120
-	id, err := future.ComputeId(hashR)
100
+	go func() {
101
+		_id, err := future.ComputeId(hashR)
102
+		id = _id
103
+		errors <- err
104
+	}()
105
+	// Duplicate archive to each stream
106
+	_, err = io.Copy(io.MultiWriter(hashW, untarW), archive)
107
+	hashW.Close()
108
+	untarW.Close()
121 109
 	if err != nil {
122 110
 		return "", err
123 111
 	}
124
-	if err := untarCmd.Wait(); err != nil {
125
-		return "", err
126
-	}
127
-	if err := <-job_copy; err != nil {
128
-		return "", err
112
+	// Wait for goroutines
113
+	for i:=0; i<2; i+=1 {
114
+		select {
115
+			case err := <-errors: {
116
+				if err != nil {
117
+					return "", err
118
+				}
119
+			}
120
+		}
129 121
 	}
130 122
 	layer := store.layerPath(id)
131 123
 	if !store.Exists(id) {
... ...
@@ -348,17 +348,9 @@ func (srv *Server) CmdKill(stdin io.ReadCloser, stdout io.Writer, args ...string
348 348
 
349 349
 func (srv *Server) CmdPull(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
350 350
 	cmd := rcli.Subcmd(stdout, "pull", "[OPTIONS] NAME", "Download a new image from a remote location")
351
-	fl_bzip2 := cmd.Bool("j", false, "Bzip2 compression")
352
-	fl_gzip := cmd.Bool("z", false, "Gzip compression")
353 351
 	if err := cmd.Parse(args); err != nil {
354 352
 		return nil
355 353
 	}
356
-	var compression image.Compression
357
-	if *fl_bzip2 {
358
-		compression = image.Bzip2
359
-	} else if *fl_gzip {
360
-		compression = image.Gzip
361
-	}
362 354
 	name := cmd.Arg(0)
363 355
 	if name == "" {
364 356
 		return errors.New("Not enough arguments")
... ...
@@ -375,12 +367,13 @@ func (srv *Server) CmdPull(stdin io.ReadCloser, stdout io.Writer, args ...string
375 375
 		u.Host = "s3.amazonaws.com"
376 376
 		u.Path = path.Join("/docker.io/images", u.Path)
377 377
 	}
378
-	fmt.Fprintf(stdout, "Downloading %s from %s...\n", name, u.String())
378
+	fmt.Fprintf(stdout, "Downloading from %s\n", u.String())
379 379
 	resp, err := http.Get(u.String())
380 380
 	if err != nil {
381 381
 		return err
382 382
 	}
383
-	img, err := srv.images.Import(name, resp.Body, stdout, nil, compression)
383
+	fmt.Fprintf(stdout, "Unpacking to %s\n", name)
384
+	img, err := srv.images.Import(name, resp.Body, nil)
384 385
 	if err != nil {
385 386
 		return err
386 387
 	}
... ...
@@ -390,22 +383,14 @@ func (srv *Server) CmdPull(stdin io.ReadCloser, stdout io.Writer, args ...string
390 390
 
391 391
 func (srv *Server) CmdPut(stdin io.ReadCloser, stdout io.Writer, args ...string) error {
392 392
 	cmd := rcli.Subcmd(stdout, "put", "[OPTIONS] NAME", "Import a new image from a local archive.")
393
-	fl_bzip2 := cmd.Bool("j", false, "Bzip2 compression")
394
-	fl_gzip := cmd.Bool("z", false, "Gzip compression")
395 393
 	if err := cmd.Parse(args); err != nil {
396 394
 		return nil
397 395
 	}
398
-	var compression image.Compression
399
-	if *fl_bzip2 {
400
-		compression = image.Bzip2
401
-	} else if *fl_gzip {
402
-		compression = image.Gzip
403
-	}
404 396
 	name := cmd.Arg(0)
405 397
 	if name == "" {
406 398
 		return errors.New("Not enough arguments")
407 399
 	}
408
-	img, err := srv.images.Import(name, stdin, stdout, nil, compression)
400
+	img, err := srv.images.Import(name, stdin, nil)
409 401
 	if err != nil {
410 402
 		return err
411 403
 	}
... ...
@@ -558,13 +543,13 @@ func (srv *Server) CmdCommit(stdin io.ReadCloser, stdout io.Writer, args ...stri
558 558
 	}
559 559
 	if container := srv.containers.Get(containerName); container != nil {
560 560
 		// FIXME: freeze the container before copying it to avoid data corruption?
561
-		rwTar, err := docker.Tar(container.Filesystem.RWPath)
561
+		rwTar, err := image.Tar(container.Filesystem.RWPath, image.Uncompressed)
562 562
 		if err != nil {
563 563
 			return err
564 564
 		}
565 565
 		// Create a new image from the container's base layers + a new layer from container changes
566 566
 		parentImg := srv.images.Find(container.GetUserData("image"))
567
-		img, err := srv.images.Import(imgName, rwTar, stdout, parentImg, image.Uncompressed)
567
+		img, err := srv.images.Import(imgName, rwTar, parentImg)
568 568
 		if err != nil {
569 569
 			return err
570 570
 		}
... ...
@@ -17,25 +17,6 @@ func Trunc(s string, maxlen int) string {
17 17
 	return s[:maxlen]
18 18
 }
19 19
 
20
-// Tar generates a tar archive from a filesystem path, and returns it as a stream.
21
-// Path must point to a directory.
22
-
23
-func Tar(path string) (io.Reader, error) {
24
-	cmd := exec.Command("tar", "-C", path, "-c", ".")
25
-	output, err := cmd.StdoutPipe()
26
-	if err != nil {
27
-		return nil, err
28
-	}
29
-	if err := cmd.Start(); err != nil {
30
-		return nil, err
31
-	}
32
-	// FIXME: errors will not be passed because we don't wait for the command.
33
-	// Instead, consumers will hit EOF right away.
34
-	// This can be fixed by waiting for the process to exit, or for the first write
35
-	// on stdout, whichever comes first.
36
-	return output, nil
37
-}
38
-
39 20
 // Figure out the absolute path of our own binary
40 21
 func SelfPath() string {
41 22
 	path, err := exec.LookPath(os.Args[0])