diff --git a/api.go b/api.go index e870ca772..563888c6b 100644 --- a/api.go +++ b/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) diff --git a/buildfile.go b/buildfile.go index aaa6f6bc7..a4e6c7f72 100644 --- a/buildfile.go +++ b/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') diff --git a/buildfile_test.go b/buildfile_test.go index d9c60a70d..0432fda3e 100644 --- a/buildfile_test.go +++ b/buildfile_test.go @@ -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) } diff --git a/commands.go b/commands.go index 04eb994f0..4441536a9 100644 --- a/commands.go +++ b/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