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:
parent
cc7de8df75
commit
38554fc2a7
4 changed files with 66 additions and 85 deletions
21
api.go
21
api.go
|
@ -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)
|
||||
|
|
29
buildfile.go
29
buildfile.go
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
88
commands.go
88
commands.go
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue