| ... | ... |
@@ -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]) |