ソースを参照

* Builder: simplify the upload of the build context. Simply stream a tarball instead of multipart upload with 4 intermediary buffers. Simpler, less memory usage, less disk usage, and faster.

Solomon Hykes 12 年 前
コミット
38554fc2a7
4 ファイル変更64 行追加83 行削除
  1. 3 18
      api.go
  2. 17 12
      buildfile.go
  3. 11 2
      buildfile_test.go
  4. 33 51
      commands.go

+ 3 - 18
api.go

@@ -13,7 +13,7 @@ import (
 	"strings"
 	"strings"
 )
 )
 
 
-const APIVERSION = 1.2
+const APIVERSION = 1.3
 
 
 func hijackServer(w http.ResponseWriter) (io.ReadCloser, io.Writer, error) {
 func hijackServer(w http.ResponseWriter) (io.ReadCloser, io.Writer, error) {
 	conn, _, err := w.(http.Hijacker).Hijack()
 	conn, _, err := w.(http.Hijacker).Hijack()
@@ -715,9 +715,7 @@ func postImagesGetCache(srv *Server, version float64, w http.ResponseWriter, r *
 }
 }
 
 
 func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
 func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
-	if err := r.ParseMultipartForm(4096); err != nil {
-		return err
-	}
+	// FIXME: "remote" is not a clear variable name.
 	remote := r.FormValue("t")
 	remote := r.FormValue("t")
 	tag := ""
 	tag := ""
 	if strings.Contains(remote, ":") {
 	if strings.Contains(remote, ":") {
@@ -725,21 +723,8 @@ func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Requ
 		tag = remoteParts[1]
 		tag = remoteParts[1]
 		remote = remoteParts[0]
 		remote = remoteParts[0]
 	}
 	}
-
-	dockerfile, _, err := r.FormFile("Dockerfile")
-	if err != nil {
-		return err
-	}
-
-	context, _, err := r.FormFile("Context")
-	if err != nil {
-		if err != http.ErrMissingFile {
-			return err
-		}
-	}
-
 	b := NewBuildFile(srv, utils.NewWriteFlusher(w))
 	b := NewBuildFile(srv, utils.NewWriteFlusher(w))
-	if id, err := b.Build(dockerfile, context); err != nil {
+	if id, err := b.Build(r.Body); err != nil {
 		fmt.Fprintf(w, "Error build: %s\n", err)
 		fmt.Fprintf(w, "Error build: %s\n", err)
 	} else if remote != "" {
 	} else if remote != "" {
 		srv.runtime.repositories.Set(remote, tag, id, false)
 		srv.runtime.repositories.Set(remote, tag, id, false)

+ 17 - 12
buildfile.go

@@ -14,7 +14,7 @@ import (
 )
 )
 
 
 type BuildFile interface {
 type BuildFile interface {
-	Build(io.Reader, io.Reader) (string, error)
+	Build(io.Reader) (string, error)
 	CmdFrom(string) error
 	CmdFrom(string) error
 	CmdRun(string) error
 	CmdRun(string) error
 }
 }
@@ -305,18 +305,23 @@ func (b *buildFile) commit(id string, autoCmd []string, comment string) error {
 	return nil
 	return nil
 }
 }
 
 
-func (b *buildFile) Build(dockerfile, context io.Reader) (string, error) {
-	if context != nil {
-		name, err := ioutil.TempDir("/tmp", "docker-build")
-		if err != nil {
-			return "", err
-		}
-		if err := Untar(context, name); err != nil {
-			return "", err
-		}
-		defer os.RemoveAll(name)
-		b.context = name
+func (b *buildFile) Build(context io.Reader) (string, error) {
+	// FIXME: @creack any reason for using /tmp instead of ""?
+	// FIXME: @creack "name" is a terrible variable name
+	name, err := ioutil.TempDir("/tmp", "docker-build")
+	if err != nil {
+		return "", err
+	}
+	if err := Untar(context, name); err != nil {
+		return "", err
+	}
+	defer os.RemoveAll(name)
+	b.context = name
+	dockerfile, err := os.Open(path.Join(name, "Dockerfile"))
+	if err != nil {
+		return "", fmt.Errorf("Can't build a directory with no Dockerfile")
 	}
 	}
+	// FIXME: "file" is also a terrible variable name ;)
 	file := bufio.NewReader(dockerfile)
 	file := bufio.NewReader(dockerfile)
 	for {
 	for {
 		line, err := file.ReadString('\n')
 		line, err := file.ReadString('\n')

+ 11 - 2
buildfile_test.go

@@ -2,7 +2,6 @@ package docker
 
 
 import (
 import (
 	"github.com/dotcloud/docker/utils"
 	"github.com/dotcloud/docker/utils"
-	"strings"
 	"testing"
 	"testing"
 )
 )
 
 
@@ -23,6 +22,16 @@ from   ` + unitTestImageName + `
 run    sh -c 'echo root:testpass > /tmp/passwd'
 run    sh -c 'echo root:testpass > /tmp/passwd'
 run    mkdir -p /var/run/sshd`
 run    mkdir -p /var/run/sshd`
 
 
+// mkTestContext generates a build context from the contents of the provided dockerfile.
+// This context is suitable for use as an argument to BuildFile.Build()
+func mkTestContext(dockerfile string, t *testing.T) Archive {
+	context, err := mkBuildContext(dockerfile)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return context
+}
+
 func TestBuild(t *testing.T) {
 func TestBuild(t *testing.T) {
 	dockerfiles := []string{Dockerfile, DockerfileNoNewLine}
 	dockerfiles := []string{Dockerfile, DockerfileNoNewLine}
 	for _, Dockerfile := range dockerfiles {
 	for _, Dockerfile := range dockerfiles {
@@ -36,7 +45,7 @@ func TestBuild(t *testing.T) {
 
 
 		buildfile := NewBuildFile(srv, &utils.NopWriter{})
 		buildfile := NewBuildFile(srv, &utils.NopWriter{})
 
 
-		imgID, err := buildfile.Build(strings.NewReader(Dockerfile), nil)
+		imgID, err := buildfile.Build(mkTestContext(Dockerfile, t))
 		if err != nil {
 		if err != nil {
 			t.Fatal(err)
 			t.Fatal(err)
 		}
 		}

+ 33 - 51
commands.go

@@ -1,6 +1,7 @@
 package docker
 package docker
 
 
 import (
 import (
+	"archive/tar"
 	"bytes"
 	"bytes"
 	"encoding/json"
 	"encoding/json"
 	"flag"
 	"flag"
@@ -10,14 +11,12 @@ import (
 	"github.com/dotcloud/docker/utils"
 	"github.com/dotcloud/docker/utils"
 	"io"
 	"io"
 	"io/ioutil"
 	"io/ioutil"
-	"mime/multipart"
 	"net"
 	"net"
 	"net/http"
 	"net/http"
 	"net/http/httputil"
 	"net/http/httputil"
 	"net/url"
 	"net/url"
 	"os"
 	"os"
 	"os/signal"
 	"os/signal"
-	"path"
 	"path/filepath"
 	"path/filepath"
 	"reflect"
 	"reflect"
 	"regexp"
 	"regexp"
@@ -131,6 +130,27 @@ func (cli *DockerCli) CmdInsert(args ...string) error {
 	return nil
 	return nil
 }
 }
 
 
+// mkBuildContext returns an archive of an empty context with the contents
+// of `dockerfile` at the path ./Dockerfile
+func mkBuildContext(content string) (Archive, error) {
+	buf := new(bytes.Buffer)
+	tw := tar.NewWriter(buf)
+	hdr := &tar.Header{
+		Name: "Dockerfile",
+		Size: int64(len(content)),
+	}
+	if err := tw.WriteHeader(hdr); err != nil {
+		return nil, err
+	}
+	if _, err := tw.Write([]byte(content)); err != nil {
+		return nil, err
+	}
+	if err := tw.Close(); err != nil {
+		return nil, err
+	}
+	return buf, nil
+}
+
 func (cli *DockerCli) CmdBuild(args ...string) error {
 func (cli *DockerCli) CmdBuild(args ...string) error {
 	cmd := Subcmd("build", "[OPTIONS] PATH | -", "Build a new container image from the source code at PATH")
 	cmd := Subcmd("build", "[OPTIONS] PATH | -", "Build a new container image from the source code at PATH")
 	tag := cmd.String("t", "", "Tag to be applied to the resulting image in case of success")
 	tag := cmd.String("t", "", "Tag to be applied to the resulting image in case of success")
@@ -143,70 +163,32 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
 	}
 	}
 
 
 	var (
 	var (
-		multipartBody io.Reader
-		file          io.ReadCloser
-		contextPath   string
+		context Archive
+		err     error
 	)
 	)
 
 
-	// Init the needed component for the Multipart
-	buff := bytes.NewBuffer([]byte{})
-	multipartBody = buff
-	w := multipart.NewWriter(buff)
-	boundary := strings.NewReader("\r\n--" + w.Boundary() + "--\r\n")
-
-	compression := Bzip2
-
 	if cmd.Arg(0) == "-" {
 	if cmd.Arg(0) == "-" {
-		file = os.Stdin
-	} else {
-		// Send Dockerfile from arg/Dockerfile (deprecate later)
-		f, err := os.Open(path.Join(cmd.Arg(0), "Dockerfile"))
-		if err != nil {
-			return err
-		}
-		file = f
-		// Send context from arg
-		// Create a FormFile multipart for the context if needed
-		// FIXME: Use NewTempArchive in order to have the size and avoid too much memory usage?
-		context, err := Tar(cmd.Arg(0), compression)
-		if err != nil {
-			return err
-		}
-		// NOTE: Do this in case '.' or '..' is input
-		absPath, err := filepath.Abs(cmd.Arg(0))
+		// As a special case, 'docker build -' will build from an empty context with the
+		// contents of stdin as a Dockerfile
+		dockerfile, err := ioutil.ReadAll(os.Stdin)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
-		wField, err := w.CreateFormFile("Context", filepath.Base(absPath)+"."+compression.Extension())
-		if err != nil {
-			return err
-		}
-		// FIXME: Find a way to have a progressbar for the upload too
-		sf := utils.NewStreamFormatter(false)
-		io.Copy(wField, utils.ProgressReader(ioutil.NopCloser(context), -1, os.Stdout, sf.FormatProgress("Caching Context", "%v/%v (%v)"), sf))
-		multipartBody = io.MultiReader(multipartBody, boundary)
+		context, err = mkBuildContext(string(dockerfile))
+	} else {
+		context, err = Tar(cmd.Arg(0), Uncompressed)
 	}
 	}
-	// Create a FormFile multipart for the Dockerfile
-	wField, err := w.CreateFormFile("Dockerfile", "Dockerfile")
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	io.Copy(wField, file)
-	multipartBody = io.MultiReader(multipartBody, boundary)
-
+	// Upload the build context
 	v := &url.Values{}
 	v := &url.Values{}
 	v.Set("t", *tag)
 	v.Set("t", *tag)
-	// Send the multipart request with correct content-type
-	req, err := http.NewRequest("POST", fmt.Sprintf("http://%s:%d%s?%s", cli.host, cli.port, "/build", v.Encode()), multipartBody)
+	req, err := http.NewRequest("POST", fmt.Sprintf("http://%s:%d%s?%s", cli.host, cli.port, "/build", v.Encode()), context)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	req.Header.Set("Content-Type", w.FormDataContentType())
-	if contextPath != "" {
-		req.Header.Set("X-Docker-Context-Compression", compression.Flag())
-		fmt.Println("Uploading Context...")
-	}
-
+	req.Header.Set("Content-Type", "application/tar")
 	resp, err := http.DefaultClient.Do(req)
 	resp, err := http.DefaultClient.Do(req)
 	if err != nil {
 	if err != nil {
 		return err
 		return err