1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00

* 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.

This commit is contained in:
Solomon Hykes 2013-06-15 09:38:18 -07:00
parent cc7de8df75
commit 38554fc2a7
4 changed files with 66 additions and 85 deletions

21
api.go
View file

@ -13,7 +13,7 @@ import (
"strings"
)
const APIVERSION = 1.2
const APIVERSION = 1.3
func hijackServer(w http.ResponseWriter) (io.ReadCloser, io.Writer, error) {
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 {
if err := r.ParseMultipartForm(4096); err != nil {
return err
}
// FIXME: "remote" is not a clear variable name.
remote := r.FormValue("t")
tag := ""
if strings.Contains(remote, ":") {
@ -725,21 +723,8 @@ func postBuild(srv *Server, version float64, w http.ResponseWriter, r *http.Requ
tag = remoteParts[1]
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))
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)
} else if remote != "" {
srv.runtime.repositories.Set(remote, tag, id, false)

View file

@ -14,7 +14,7 @@ import (
)
type BuildFile interface {
Build(io.Reader, io.Reader) (string, error)
Build(io.Reader) (string, error)
CmdFrom(string) error
CmdRun(string) error
}
@ -305,18 +305,23 @@ func (b *buildFile) commit(id string, autoCmd []string, comment string) error {
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)
for {
line, err := file.ReadString('\n')

View file

@ -2,7 +2,6 @@ package docker
import (
"github.com/dotcloud/docker/utils"
"strings"
"testing"
)
@ -23,6 +22,16 @@ from ` + unitTestImageName + `
run sh -c 'echo root:testpass > /tmp/passwd'
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) {
dockerfiles := []string{Dockerfile, DockerfileNoNewLine}
for _, Dockerfile := range dockerfiles {
@ -36,7 +45,7 @@ func TestBuild(t *testing.T) {
buildfile := NewBuildFile(srv, &utils.NopWriter{})
imgID, err := buildfile.Build(strings.NewReader(Dockerfile), nil)
imgID, err := buildfile.Build(mkTestContext(Dockerfile, t))
if err != nil {
t.Fatal(err)
}

View file

@ -1,6 +1,7 @@
package docker
import (
"archive/tar"
"bytes"
"encoding/json"
"flag"
@ -10,14 +11,12 @@ import (
"github.com/dotcloud/docker/utils"
"io"
"io/ioutil"
"mime/multipart"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/signal"
"path"
"path/filepath"
"reflect"
"regexp"
@ -131,6 +130,27 @@ func (cli *DockerCli) CmdInsert(args ...string) error {
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 {
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")
@ -143,70 +163,32 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
}
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) == "-" {
file = os.Stdin
// 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 {
return err
}
context, err = mkBuildContext(string(dockerfile))
} 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))
if err != nil {
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 = Tar(cmd.Arg(0), Uncompressed)
}
// Create a FormFile multipart for the Dockerfile
wField, err := w.CreateFormFile("Dockerfile", "Dockerfile")
if err != nil {
return err
}
io.Copy(wField, file)
multipartBody = io.MultiReader(multipartBody, boundary)
// Upload the build context
v := &url.Values{}
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 {
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)
if err != nil {
return err