Browse Source

Merge pull request #848 from dotcloud/builder_server-3

Improve Docker build
Guillaume J. Charmes 12 years ago
parent
commit
de1a5a75cc

+ 1 - 0
FIXME

@@ -33,3 +33,4 @@ to put them - so we put them here :)
 * Caching after an ADD
 * Caching after an ADD
 * entry point config
 * entry point config
 * bring back git revision info, looks like it was lost
 * bring back git revision info, looks like it was lost
+* Clean up the ProgressReader api, it's a PITA to use

+ 51 - 18
api.go

@@ -7,15 +7,17 @@ import (
 	"github.com/dotcloud/docker/utils"
 	"github.com/dotcloud/docker/utils"
 	"github.com/gorilla/mux"
 	"github.com/gorilla/mux"
 	"io"
 	"io"
+	"io/ioutil"
 	"log"
 	"log"
 	"net"
 	"net"
 	"net/http"
 	"net/http"
 	"os"
 	"os"
+	"os/exec"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 )
 )
 
 
-const APIVERSION = 1.2
+const APIVERSION = 1.3
 const DEFAULTHTTPHOST string = "127.0.0.1"
 const DEFAULTHTTPHOST string = "127.0.0.1"
 const DEFAULTHTTPPORT int = 4243
 const DEFAULTHTTPPORT int = 4243
 
 
@@ -723,34 +725,65 @@ 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
+	if version < 1.3 {
+		return fmt.Errorf("Multipart upload for build is no longer supported. Please upgrade your docker client.")
 	}
 	}
-	remote := r.FormValue("t")
+	remoteURL := r.FormValue("remote")
+	repoName := r.FormValue("t")
 	tag := ""
 	tag := ""
-	if strings.Contains(remote, ":") {
-		remoteParts := strings.Split(remote, ":")
+	if strings.Contains(repoName, ":") {
+		remoteParts := strings.Split(repoName, ":")
 		tag = remoteParts[1]
 		tag = remoteParts[1]
-		remote = remoteParts[0]
+		repoName = remoteParts[0]
 	}
 	}
 
 
-	dockerfile, _, err := r.FormFile("Dockerfile")
-	if err != nil {
-		return err
-	}
+	var context io.Reader
 
 
-	context, _, err := r.FormFile("Context")
-	if err != nil {
-		if err != http.ErrMissingFile {
+	if remoteURL == "" {
+		context = r.Body
+	} else if utils.IsGIT(remoteURL) {
+		if !strings.HasPrefix(remoteURL, "git://") {
+			remoteURL = "https://" + remoteURL
+		}
+		root, err := ioutil.TempDir("", "docker-build-git")
+		if err != nil {
 			return err
 			return err
 		}
 		}
-	}
+		defer os.RemoveAll(root)
+
+		if output, err := exec.Command("git", "clone", remoteURL, root).CombinedOutput(); err != nil {
+			return fmt.Errorf("Error trying to use git: %s (%s)", err, output)
+		}
 
 
+		c, err := Tar(root, Bzip2)
+		if err != nil {
+			return err
+		}
+		context = c
+	} else if utils.IsURL(remoteURL) {
+		f, err := utils.Download(remoteURL, ioutil.Discard)
+		if err != nil {
+			return err
+		}
+		defer f.Body.Close()
+		dockerFile, err := ioutil.ReadAll(f.Body)
+		if err != nil {
+			return err
+		}
+		c, err := mkBuildContext(string(dockerFile), nil)
+		if err != nil {
+			return err
+		}
+		context = c
+	}
 	b := NewBuildFile(srv, utils.NewWriteFlusher(w))
 	b := NewBuildFile(srv, utils.NewWriteFlusher(w))
-	if id, err := b.Build(dockerfile, context); err != nil {
+	id, err := b.Build(context)
+	if err != nil {
 		fmt.Fprintf(w, "Error build: %s\n", err)
 		fmt.Fprintf(w, "Error build: %s\n", err)
-	} else if remote != "" {
-		srv.runtime.repositories.Set(remote, tag, id, false)
+		return err
+	}
+	if repoName != "" {
+		srv.runtime.repositories.Set(repoName, tag, id, false)
 	}
 	}
 	return nil
 	return nil
 }
 }

+ 50 - 38
archive.go

@@ -1,7 +1,9 @@
 package docker
 package docker
 
 
 import (
 import (
+	"archive/tar"
 	"bufio"
 	"bufio"
+	"bytes"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"github.com/dotcloud/docker/utils"
 	"github.com/dotcloud/docker/utils"
@@ -10,6 +12,7 @@ import (
 	"os"
 	"os"
 	"os/exec"
 	"os/exec"
 	"path"
 	"path"
+	"path/filepath"
 )
 )
 
 
 type Archive io.Reader
 type Archive io.Reader
@@ -160,51 +163,60 @@ func CopyWithTar(src, dst string) error {
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	var dstExists bool
-	dstSt, err := os.Stat(dst)
+	if !srcSt.IsDir() {
+		return CopyFileWithTar(src, dst)
+	}
+	// Create dst, copy src's content into it
+	utils.Debugf("Creating dest directory: %s", dst)
+	if err := os.MkdirAll(dst, 0700); err != nil && !os.IsExist(err) {
+		return err
+	}
+	utils.Debugf("Calling TarUntar(%s, %s)", src, dst)
+	return TarUntar(src, nil, dst)
+}
+
+// CopyFileWithTar emulates the behavior of the 'cp' command-line
+// for a single file. It copies a regular file from path `src` to
+// path `dst`, and preserves all its metadata.
+//
+// If `dst` ends with a trailing slash '/', the final destination path
+// will be `dst/base(src)`.
+func CopyFileWithTar(src, dst string) error {
+	utils.Debugf("CopyFileWithTar(%s, %s)", src, dst)
+	srcSt, err := os.Stat(src)
 	if err != nil {
 	if err != nil {
-		if !os.IsNotExist(err) {
-			return err
-		}
-	} else {
-		dstExists = true
+		return err
 	}
 	}
-	// Things that can go wrong if the source is a directory
 	if srcSt.IsDir() {
 	if srcSt.IsDir() {
-		// The destination exists and is a regular file
-		if dstExists && !dstSt.IsDir() {
-			return fmt.Errorf("Can't copy a directory over a regular file")
-		}
-		// Things that can go wrong if the source is a regular file
-	} else {
-		utils.Debugf("The destination exists, it's a directory, and doesn't end in /")
-		// The destination exists, it's a directory, and doesn't end in /
-		if dstExists && dstSt.IsDir() && dst[len(dst)-1] != '/' {
-			return fmt.Errorf("Can't copy a regular file over a directory %s |%s|", dst, dst[len(dst)-1])
-		}
+		return fmt.Errorf("Can't copy a directory")
 	}
 	}
-	// Create the destination
-	var dstDir string
-	if srcSt.IsDir() || dst[len(dst)-1] == '/' {
-		// The destination ends in /, or the source is a directory
-		//   --> dst is the holding directory and needs to be created for -C
-		dstDir = dst
-	} else {
-		// The destination doesn't end in /
-		//   --> dst is the file
-		dstDir = path.Dir(dst)
+	// Clean up the trailing /
+	if dst[len(dst)-1] == '/' {
+		dst = path.Join(dst, filepath.Base(src))
 	}
 	}
-	if !dstExists {
-		// Create the holding directory if necessary
-		utils.Debugf("Creating the holding directory %s", dstDir)
-		if err := os.MkdirAll(dstDir, 0700); err != nil && !os.IsExist(err) {
-			return err
-		}
+	// Create the holding directory if necessary
+	if err := os.MkdirAll(filepath.Dir(dst), 0700); err != nil && !os.IsExist(err) {
+		return err
 	}
 	}
-	if !srcSt.IsDir() {
-		return TarUntar(path.Dir(src), []string{path.Base(src)}, dstDir)
+	buf := new(bytes.Buffer)
+	tw := tar.NewWriter(buf)
+	hdr, err := tar.FileInfoHeader(srcSt, "")
+	if err != nil {
+		return err
+	}
+	hdr.Name = filepath.Base(dst)
+	if err := tw.WriteHeader(hdr); err != nil {
+		return err
+	}
+	srcF, err := os.Open(src)
+	if err != nil {
+		return err
+	}
+	if _, err := io.Copy(tw, srcF); err != nil {
+		return err
 	}
 	}
-	return TarUntar(src, nil, dstDir)
+	tw.Close()
+	return Untar(buf, filepath.Dir(dst))
 }
 }
 
 
 // CmdStream executes a command, and returns its stdout as a stream.
 // CmdStream executes a command, and returns its stdout as a stream.

+ 0 - 314
builder_client.go

@@ -1,314 +0,0 @@
-package docker
-
-import (
-	"bufio"
-	"encoding/json"
-	"fmt"
-	"github.com/dotcloud/docker/utils"
-	"io"
-	"net/url"
-	"os"
-	"reflect"
-	"strings"
-)
-
-type builderClient struct {
-	cli *DockerCli
-
-	image      string
-	maintainer string
-	config     *Config
-
-	tmpContainers map[string]struct{}
-	tmpImages     map[string]struct{}
-
-	needCommit bool
-}
-
-func (b *builderClient) clearTmp(containers, images map[string]struct{}) {
-	for i := range images {
-		if _, _, err := b.cli.call("DELETE", "/images/"+i, nil); err != nil {
-			utils.Debugf("%s", err)
-		}
-		utils.Debugf("Removing image %s", i)
-	}
-}
-
-func (b *builderClient) CmdFrom(name string) error {
-	obj, statusCode, err := b.cli.call("GET", "/images/"+name+"/json", nil)
-	if statusCode == 404 {
-
-		remote := name
-		var tag string
-		if strings.Contains(remote, ":") {
-			remoteParts := strings.Split(remote, ":")
-			tag = remoteParts[1]
-			remote = remoteParts[0]
-		}
-		var out io.Writer
-		if os.Getenv("DEBUG") != "" {
-			out = os.Stdout
-		} else {
-			out = &utils.NopWriter{}
-		}
-		if err := b.cli.stream("POST", "/images/create?fromImage="+remote+"&tag="+tag, nil, out); err != nil {
-			return err
-		}
-		obj, _, err = b.cli.call("GET", "/images/"+name+"/json", nil)
-		if err != nil {
-			return err
-		}
-	}
-	if err != nil {
-		return err
-	}
-
-	img := &APIID{}
-	if err := json.Unmarshal(obj, img); err != nil {
-		return err
-	}
-	b.image = img.ID
-	utils.Debugf("Using image %s", b.image)
-	return nil
-}
-
-func (b *builderClient) CmdMaintainer(name string) error {
-	b.needCommit = true
-	b.maintainer = name
-	return nil
-}
-
-func (b *builderClient) CmdRun(args string) error {
-	if b.image == "" {
-		return fmt.Errorf("Please provide a source image with `from` prior to run")
-	}
-	config, _, err := ParseRun([]string{b.image, "/bin/sh", "-c", args}, nil)
-	if err != nil {
-		return err
-	}
-
-	cmd, env := b.config.Cmd, b.config.Env
-	b.config.Cmd = nil
-	MergeConfig(b.config, config)
-
-	body, statusCode, err := b.cli.call("POST", "/images/getCache", &APIImageConfig{ID: b.image, Config: b.config})
-	if err != nil {
-		if statusCode != 404 {
-			return err
-		}
-	}
-	if statusCode != 404 {
-		apiID := &APIID{}
-		if err := json.Unmarshal(body, apiID); err != nil {
-			return err
-		}
-		utils.Debugf("Use cached version")
-		b.image = apiID.ID
-		return nil
-	}
-	cid, err := b.run()
-	if err != nil {
-		return err
-	}
-	b.config.Cmd, b.config.Env = cmd, env
-	return b.commit(cid)
-}
-
-func (b *builderClient) CmdEnv(args string) error {
-	b.needCommit = true
-	tmp := strings.SplitN(args, " ", 2)
-	if len(tmp) != 2 {
-		return fmt.Errorf("Invalid ENV format")
-	}
-	key := strings.Trim(tmp[0], " ")
-	value := strings.Trim(tmp[1], " ")
-
-	for i, elem := range b.config.Env {
-		if strings.HasPrefix(elem, key+"=") {
-			b.config.Env[i] = key + "=" + value
-			return nil
-		}
-	}
-	b.config.Env = append(b.config.Env, key+"="+value)
-	return nil
-}
-
-func (b *builderClient) CmdCmd(args string) error {
-	b.needCommit = true
-	var cmd []string
-	if err := json.Unmarshal([]byte(args), &cmd); err != nil {
-		utils.Debugf("Error unmarshalling: %s, using /bin/sh -c", err)
-		b.config.Cmd = []string{"/bin/sh", "-c", args}
-	} else {
-		b.config.Cmd = cmd
-	}
-	return nil
-}
-
-func (b *builderClient) CmdExpose(args string) error {
-	ports := strings.Split(args, " ")
-	b.config.PortSpecs = append(ports, b.config.PortSpecs...)
-	return nil
-}
-
-func (b *builderClient) CmdInsert(args string) error {
-	// tmp := strings.SplitN(args, "\t ", 2)
-	// sourceUrl, destPath := tmp[0], tmp[1]
-
-	// v := url.Values{}
-	// v.Set("url", sourceUrl)
-	// v.Set("path", destPath)
-	// body, _, err := b.cli.call("POST", "/images/insert?"+v.Encode(), nil)
-	// if err != nil {
-	// 	return err
-	// }
-
-	// apiId := &APIId{}
-	// if err := json.Unmarshal(body, apiId); err != nil {
-	// 	return err
-	// }
-
-	// FIXME: Reimplement this, we need to retrieve the resulting Id
-	return fmt.Errorf("INSERT not implemented")
-}
-
-func (b *builderClient) run() (string, error) {
-	if b.image == "" {
-		return "", fmt.Errorf("Please provide a source image with `from` prior to run")
-	}
-	b.config.Image = b.image
-	body, _, err := b.cli.call("POST", "/containers/create", b.config)
-	if err != nil {
-		return "", err
-	}
-
-	apiRun := &APIRun{}
-	if err := json.Unmarshal(body, apiRun); err != nil {
-		return "", err
-	}
-	for _, warning := range apiRun.Warnings {
-		fmt.Fprintln(os.Stderr, "WARNING: ", warning)
-	}
-
-	//start the container
-	_, _, err = b.cli.call("POST", "/containers/"+apiRun.ID+"/start", nil)
-	if err != nil {
-		return "", err
-	}
-	b.tmpContainers[apiRun.ID] = struct{}{}
-
-	// Wait for it to finish
-	body, _, err = b.cli.call("POST", "/containers/"+apiRun.ID+"/wait", nil)
-	if err != nil {
-		return "", err
-	}
-	apiWait := &APIWait{}
-	if err := json.Unmarshal(body, apiWait); err != nil {
-		return "", err
-	}
-	if apiWait.StatusCode != 0 {
-		return "", fmt.Errorf("The command %v returned a non-zero code: %d", b.config.Cmd, apiWait.StatusCode)
-	}
-
-	return apiRun.ID, nil
-}
-
-func (b *builderClient) commit(id string) error {
-	if b.image == "" {
-		return fmt.Errorf("Please provide a source image with `from` prior to run")
-	}
-	b.config.Image = b.image
-
-	if id == "" {
-		cmd := b.config.Cmd
-		b.config.Cmd = []string{"true"}
-		cid, err := b.run()
-		if err != nil {
-			return err
-		}
-		id = cid
-		b.config.Cmd = cmd
-	}
-
-	// Commit the container
-	v := url.Values{}
-	v.Set("container", id)
-	v.Set("author", b.maintainer)
-
-	body, _, err := b.cli.call("POST", "/commit?"+v.Encode(), b.config)
-	if err != nil {
-		return err
-	}
-	apiID := &APIID{}
-	if err := json.Unmarshal(body, apiID); err != nil {
-		return err
-	}
-	b.tmpImages[apiID.ID] = struct{}{}
-	b.image = apiID.ID
-	b.needCommit = false
-	return nil
-}
-
-func (b *builderClient) Build(dockerfile, context io.Reader) (string, error) {
-	defer b.clearTmp(b.tmpContainers, b.tmpImages)
-	file := bufio.NewReader(dockerfile)
-	for {
-		line, err := file.ReadString('\n')
-		if err != nil {
-			if err == io.EOF {
-				break
-			}
-			return "", err
-		}
-		line = strings.Replace(strings.TrimSpace(line), "	", " ", 1)
-		// Skip comments and empty line
-		if len(line) == 0 || line[0] == '#' {
-			continue
-		}
-		tmp := strings.SplitN(line, " ", 2)
-		if len(tmp) != 2 {
-			return "", fmt.Errorf("Invalid Dockerfile format")
-		}
-		instruction := strings.ToLower(strings.Trim(tmp[0], " "))
-		arguments := strings.Trim(tmp[1], " ")
-
-		fmt.Fprintf(os.Stderr, "%s %s (%s)\n", strings.ToUpper(instruction), arguments, b.image)
-
-		method, exists := reflect.TypeOf(b).MethodByName("Cmd" + strings.ToUpper(instruction[:1]) + strings.ToLower(instruction[1:]))
-		if !exists {
-			fmt.Fprintf(os.Stderr, "Skipping unknown instruction %s\n", strings.ToUpper(instruction))
-		}
-		ret := method.Func.Call([]reflect.Value{reflect.ValueOf(b), reflect.ValueOf(arguments)})[0].Interface()
-		if ret != nil {
-			return "", ret.(error)
-		}
-
-		fmt.Fprintf(os.Stderr, "===> %v\n", b.image)
-	}
-	if b.needCommit {
-		if err := b.commit(""); err != nil {
-			return "", err
-		}
-	}
-	if b.image != "" {
-		// The build is successful, keep the temporary containers and images
-		for i := range b.tmpImages {
-			delete(b.tmpImages, i)
-		}
-		for i := range b.tmpContainers {
-			delete(b.tmpContainers, i)
-		}
-		fmt.Fprintf(os.Stderr, "Build finished. image id: %s\n", b.image)
-		return b.image, nil
-	}
-	return "", fmt.Errorf("An error occured during the build\n")
-}
-
-func NewBuilderClient(proto, addr string) BuildFile {
-	return &builderClient{
-		cli:           NewDockerCli(proto, addr),
-		config:        &Config{},
-		tmpContainers: make(map[string]struct{}),
-		tmpImages:     make(map[string]struct{}),
-	}
-}

+ 69 - 43
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
 }
 }
@@ -125,8 +125,8 @@ func (b *buildFile) CmdEnv(args string) error {
 	if len(tmp) != 2 {
 	if len(tmp) != 2 {
 		return fmt.Errorf("Invalid ENV format")
 		return fmt.Errorf("Invalid ENV format")
 	}
 	}
-	key := strings.Trim(tmp[0], " ")
-	value := strings.Trim(tmp[1], " ")
+	key := strings.Trim(tmp[0], " \t")
+	value := strings.Trim(tmp[1], " \t")
 
 
 	for i, elem := range b.config.Env {
 	for i, elem := range b.config.Env {
 		if strings.HasPrefix(elem, key+"=") {
 		if strings.HasPrefix(elem, key+"=") {
@@ -165,34 +165,17 @@ func (b *buildFile) CmdCopy(args string) error {
 	return fmt.Errorf("COPY has been deprecated. Please use ADD instead")
 	return fmt.Errorf("COPY has been deprecated. Please use ADD instead")
 }
 }
 
 
-func (b *buildFile) CmdAdd(args string) error {
-	if b.context == "" {
-		return fmt.Errorf("No context given. Impossible to use ADD")
-	}
-	tmp := strings.SplitN(args, " ", 2)
-	if len(tmp) != 2 {
-		return fmt.Errorf("Invalid ADD format")
-	}
-	orig := strings.Trim(tmp[0], " ")
-	dest := strings.Trim(tmp[1], " ")
-
-	cmd := b.config.Cmd
-
-	// Create the container and start it
-	b.config.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("#(nop) ADD %s in %s", orig, dest)}
-	b.config.Image = b.image
-	container, err := b.builder.Create(b.config)
+func (b *buildFile) addRemote(container *Container, orig, dest string) error {
+	file, err := utils.Download(orig, ioutil.Discard)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	b.tmpContainers[container.ID] = struct{}{}
-	fmt.Fprintf(b.out, " ---> Running in %s\n", utils.TruncateID(container.ID))
+	defer file.Body.Close()
 
 
-	if err := container.EnsureMounted(); err != nil {
-		return err
-	}
-	defer container.Unmount()
+	return container.Inject(file.Body, dest)
+}
 
 
+func (b *buildFile) addContext(container *Container, orig, dest string) error {
 	origPath := path.Join(b.context, orig)
 	origPath := path.Join(b.context, orig)
 	destPath := path.Join(container.RootfsPath(), dest)
 	destPath := path.Join(container.RootfsPath(), dest)
 	// Preserve the trailing '/'
 	// Preserve the trailing '/'
@@ -218,6 +201,46 @@ func (b *buildFile) CmdAdd(args string) error {
 			return err
 			return err
 		}
 		}
 	}
 	}
+	return nil
+}
+
+func (b *buildFile) CmdAdd(args string) error {
+	if b.context == "" {
+		return fmt.Errorf("No context given. Impossible to use ADD")
+	}
+	tmp := strings.SplitN(args, " ", 2)
+	if len(tmp) != 2 {
+		return fmt.Errorf("Invalid ADD format")
+	}
+	orig := strings.Trim(tmp[0], " \t")
+	dest := strings.Trim(tmp[1], " \t")
+
+	cmd := b.config.Cmd
+	b.config.Cmd = []string{"/bin/sh", "-c", fmt.Sprintf("#(nop) ADD %s in %s", orig, dest)}
+
+	b.config.Image = b.image
+	// Create the container and start it
+	container, err := b.builder.Create(b.config)
+	if err != nil {
+		return err
+	}
+	b.tmpContainers[container.ID] = struct{}{}
+
+	if err := container.EnsureMounted(); err != nil {
+		return err
+	}
+	defer container.Unmount()
+
+	if utils.IsURL(orig) {
+		if err := b.addRemote(container, orig, dest); err != nil {
+			return err
+		}
+	} else {
+		if err := b.addContext(container, orig, dest); err != nil {
+			return err
+		}
+	}
+
 	if err := b.commit(container.ID, cmd, fmt.Sprintf("ADD %s in %s", orig, dest)); err != nil {
 	if err := b.commit(container.ID, cmd, fmt.Sprintf("ADD %s in %s", orig, dest)); err != nil {
 		return err
 		return err
 	}
 	}
@@ -259,7 +282,9 @@ func (b *buildFile) commit(id string, autoCmd []string, comment string) error {
 	}
 	}
 	b.config.Image = b.image
 	b.config.Image = b.image
 	if id == "" {
 	if id == "" {
+		cmd := b.config.Cmd
 		b.config.Cmd = []string{"/bin/sh", "-c", "#(nop) " + comment}
 		b.config.Cmd = []string{"/bin/sh", "-c", "#(nop) " + comment}
+		defer func(cmd []string) { b.config.Cmd = cmd }(cmd)
 
 
 		if cache, err := b.srv.ImageGetCached(b.image, b.config); err != nil {
 		if cache, err := b.srv.ImageGetCached(b.image, b.config); err != nil {
 			return err
 			return err
@@ -271,21 +296,17 @@ func (b *buildFile) commit(id string, autoCmd []string, comment string) error {
 		} else {
 		} else {
 			utils.Debugf("[BUILDER] Cache miss")
 			utils.Debugf("[BUILDER] Cache miss")
 		}
 		}
-
-		// Create the container and start it
 		container, err := b.builder.Create(b.config)
 		container, err := b.builder.Create(b.config)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 		b.tmpContainers[container.ID] = struct{}{}
 		b.tmpContainers[container.ID] = struct{}{}
 		fmt.Fprintf(b.out, " ---> Running in %s\n", utils.TruncateID(container.ID))
 		fmt.Fprintf(b.out, " ---> Running in %s\n", utils.TruncateID(container.ID))
-
+		id = container.ID
 		if err := container.EnsureMounted(); err != nil {
 		if err := container.EnsureMounted(); err != nil {
 			return err
 			return err
 		}
 		}
 		defer container.Unmount()
 		defer container.Unmount()
-
-		id = container.ID
 	}
 	}
 
 
 	container := b.runtime.Get(id)
 	container := b.runtime.Get(id)
@@ -306,18 +327,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)
 	stepN := 0
 	stepN := 0
 	for {
 	for {
@@ -329,7 +355,7 @@ func (b *buildFile) Build(dockerfile, context io.Reader) (string, error) {
 				return "", err
 				return "", err
 			}
 			}
 		}
 		}
-		line = strings.Replace(strings.TrimSpace(line), "	", " ", 1)
+		line = strings.Trim(strings.Replace(line, "\t", " ", -1), " \t\r\n")
 		// Skip comments and empty line
 		// Skip comments and empty line
 		if len(line) == 0 || line[0] == '#' {
 		if len(line) == 0 || line[0] == '#' {
 			continue
 			continue

+ 77 - 64
buildfile_test.go

@@ -1,37 +1,91 @@
 package docker
 package docker
 
 
 import (
 import (
-	"github.com/dotcloud/docker/utils"
-	"strings"
+	"io/ioutil"
 	"testing"
 	"testing"
 )
 )
 
 
-const Dockerfile = `
-# VERSION		0.1
-# DOCKER-VERSION	0.2
-
-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, files [][2]string, t *testing.T) Archive {
+	context, err := mkBuildContext(dockerfile, files)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return context
+}
 
 
-const DockerfileNoNewLine = `
-# VERSION		0.1
-# DOCKER-VERSION	0.2
+// A testContextTemplate describes a build context and how to test it
+type testContextTemplate struct {
+	// Contents of the Dockerfile
+	dockerfile string
+	// Additional files in the context, eg [][2]string{"./passwd", "gordon"}
+	files [][2]string
+}
 
 
-from   ` + unitTestImageName + `
+// A table of all the contexts to build and test.
+// A new docker runtime will be created and torn down for each context.
+var testContexts []testContextTemplate = []testContextTemplate{
+	{
+		`
+from   docker-ut
 run    sh -c 'echo root:testpass > /tmp/passwd'
 run    sh -c 'echo root:testpass > /tmp/passwd'
-run    mkdir -p /var/run/sshd`
-
-// FIXME: test building with a context
-
-// FIXME: test building with a local ADD as first command
+run    mkdir -p /var/run/sshd
+run    [ "$(cat /tmp/passwd)" = "root:testpass" ]
+run    [ "$(ls -d /var/run/sshd)" = "/var/run/sshd" ]
+`,
+		nil,
+	},
+
+	{
+		`
+from docker-ut
+add foo /usr/lib/bla/bar
+run [ "$(cat /usr/lib/bla/bar)" = 'hello world!' ]
+`,
+		[][2]string{{"foo", "hello world!"}},
+	},
+
+	{
+		`
+from docker-ut
+add f /
+run [ "$(cat /f)" = "hello" ]
+add f /abc
+run [ "$(cat /abc)" = "hello" ]
+add f /x/y/z
+run [ "$(cat /x/y/z)" = "hello" ]
+add f /x/y/d/
+run [ "$(cat /x/y/d/f)" = "hello" ]
+add d /
+run [ "$(cat /ga)" = "bu" ]
+add d /somewhere
+run [ "$(cat /somewhere/ga)" = "bu" ]
+add d /anotherplace/
+run [ "$(cat /anotherplace/ga)" = "bu" ]
+add d /somewheeeere/over/the/rainbooow
+run [ "$(cat /somewheeeere/over/the/rainbooow/ga)" = "bu" ]
+`,
+		[][2]string{
+			{"f", "hello"},
+			{"d/ga", "bu"},
+		},
+	},
+
+	{
+		`
+from docker-ut
+env    FOO BAR
+run    [ "$FOO" = "BAR" ]
+`,
+		nil,
+	},
+}
 
 
 // FIXME: test building with 2 successive overlapping ADD commands
 // FIXME: test building with 2 successive overlapping ADD commands
 
 
 func TestBuild(t *testing.T) {
 func TestBuild(t *testing.T) {
-	dockerfiles := []string{Dockerfile, DockerfileNoNewLine}
-	for _, Dockerfile := range dockerfiles {
+	for _, ctx := range testContexts {
 		runtime, err := newTestRuntime()
 		runtime, err := newTestRuntime()
 		if err != nil {
 		if err != nil {
 			t.Fatal(err)
 			t.Fatal(err)
@@ -40,50 +94,9 @@ func TestBuild(t *testing.T) {
 
 
 		srv := &Server{runtime: runtime}
 		srv := &Server{runtime: runtime}
 
 
-		buildfile := NewBuildFile(srv, &utils.NopWriter{})
-
-		imgID, err := buildfile.Build(strings.NewReader(Dockerfile), nil)
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		builder := NewBuilder(runtime)
-		container, err := builder.Create(
-			&Config{
-				Image: imgID,
-				Cmd:   []string{"cat", "/tmp/passwd"},
-			},
-		)
-		if err != nil {
-			t.Fatal(err)
-		}
-		defer runtime.Destroy(container)
-
-		output, err := container.Output()
-		if err != nil {
+		buildfile := NewBuildFile(srv, ioutil.Discard)
+		if _, err := buildfile.Build(mkTestContext(ctx.dockerfile, ctx.files, t)); err != nil {
 			t.Fatal(err)
 			t.Fatal(err)
 		}
 		}
-		if string(output) != "root:testpass\n" {
-			t.Fatalf("Unexpected output. Read '%s', expected '%s'", output, "root:testpass\n")
-		}
-
-		container2, err := builder.Create(
-			&Config{
-				Image: imgID,
-				Cmd:   []string{"ls", "-d", "/var/run/sshd"},
-			},
-		)
-		if err != nil {
-			t.Fatal(err)
-		}
-		defer runtime.Destroy(container2)
-
-		output, err = container2.Output()
-		if err != nil {
-			t.Fatal(err)
-		}
-		if string(output) != "/var/run/sshd\n" {
-			t.Fatal("/var/run/sshd has not been created")
-		}
 	}
 	}
 }
 }

+ 51 - 53
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,8 +130,33 @@ 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(dockerfile string, files [][2]string) (Archive, error) {
+	buf := new(bytes.Buffer)
+	tw := tar.NewWriter(buf)
+	files = append(files, [2]string{"Dockerfile", dockerfile})
+	for _, file := range files {
+		name, content := file[0], file[1]
+		hdr := &tar.Header{
+			Name: name,
+			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 | URL | -", "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")
 	if err := cmd.Parse(args); err != nil {
 	if err := cmd.Parse(args); err != nil {
 		return nil
 		return nil
@@ -143,68 +167,43 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
 	}
 	}
 
 
 	var (
 	var (
-		multipartBody io.Reader
-		file          io.ReadCloser
-		contextPath   string
+		context  Archive
+		isRemote bool
+		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"))
+		// 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
 		}
 		}
-		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 = mkBuildContext(string(dockerfile), nil)
+	} else if utils.IsURL(cmd.Arg(0)) || utils.IsGIT(cmd.Arg(0)) {
+		isRemote = true
+	} else {
+		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
+	var body io.Reader
+	// Setup an upload progress bar
+	// FIXME: ProgressReader shouldn't be this annoyning to use
+	if context != nil {
+		sf := utils.NewStreamFormatter(false)
+		body = utils.ProgressReader(ioutil.NopCloser(context), 0, os.Stderr, sf.FormatProgress("Uploading context", "%v bytes%0.0s%0.0s"), sf)
 	}
 	}
-	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("/v%g/build?%s", APIVERSION, v.Encode()), multipartBody)
+	if isRemote {
+		v.Set("remote", cmd.Arg(0))
+	}
+	req, err := http.NewRequest("POST", fmt.Sprintf("/v%g/build?%s", APIVERSION, v.Encode()), body)
 	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...")
+	if context != nil {
+		req.Header.Set("Content-Type", "application/tar")
 	}
 	}
 	dial, err := net.Dial(cli.proto, cli.addr)
 	dial, err := net.Dial(cli.proto, cli.addr)
 	if err != nil {
 	if err != nil {
@@ -217,7 +216,6 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
 		return err
 		return err
 	}
 	}
 	defer resp.Body.Close()
 	defer resp.Body.Close()
-
 	// Check for errors
 	// Check for errors
 	if resp.StatusCode < 200 || resp.StatusCode >= 400 {
 	if resp.StatusCode < 200 || resp.StatusCode >= 400 {
 		body, err := ioutil.ReadAll(resp.Body)
 		body, err := ioutil.ReadAll(resp.Body)

+ 2 - 3
docker/docker.go

@@ -126,7 +126,7 @@ func daemon(pidfile string, protoAddrs []string, autoRestart, enableCors bool, f
 	for _, protoAddr := range protoAddrs {
 	for _, protoAddr := range protoAddrs {
 		protoAddrParts := strings.SplitN(protoAddr, "://", 2)
 		protoAddrParts := strings.SplitN(protoAddr, "://", 2)
 		if protoAddrParts[0] == "unix" {
 		if protoAddrParts[0] == "unix" {
-			syscall.Unlink(protoAddrParts[1]);
+			syscall.Unlink(protoAddrParts[1])
 		} else if protoAddrParts[0] == "tcp" {
 		} else if protoAddrParts[0] == "tcp" {
 			if !strings.HasPrefix(protoAddrParts[1], "127.0.0.1") {
 			if !strings.HasPrefix(protoAddrParts[1], "127.0.0.1") {
 				log.Println("/!\\ DON'T BIND ON ANOTHER IP ADDRESS THAN 127.0.0.1 IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\")
 				log.Println("/!\\ DON'T BIND ON ANOTHER IP ADDRESS THAN 127.0.0.1 IF YOU DON'T KNOW WHAT YOU'RE DOING /!\\")
@@ -139,7 +139,7 @@ func daemon(pidfile string, protoAddrs []string, autoRestart, enableCors bool, f
 			chErrors <- docker.ListenAndServe(protoAddrParts[0], protoAddrParts[1], server, true)
 			chErrors <- docker.ListenAndServe(protoAddrParts[0], protoAddrParts[1], server, true)
 		}()
 		}()
 	}
 	}
-	for i :=0 ; i < len(protoAddrs); i+=1 {
+	for i := 0; i < len(protoAddrs); i += 1 {
 		err := <-chErrors
 		err := <-chErrors
 		if err != nil {
 		if err != nil {
 			return err
 			return err
@@ -147,4 +147,3 @@ func daemon(pidfile string, protoAddrs []string, autoRestart, enableCors bool, f
 	}
 	}
 	return nil
 	return nil
 }
 }
-

+ 17 - 2
docs/sources/api/docker_remote_api.rst

@@ -19,10 +19,25 @@ Docker Remote API
 2. Versions
 2. Versions
 ===========
 ===========
 
 
-The current verson of the API is 1.2
-Calling /images/<name>/insert is the same as calling /v1.2/images/<name>/insert
+The current verson of the API is 1.3
+Calling /images/<name>/insert is the same as calling /v1.3/images/<name>/insert
 You can still call an old version of the api using /v1.0/images/<name>/insert
 You can still call an old version of the api using /v1.0/images/<name>/insert
 
 
+:doc:`docker_remote_api_v1.3`
+*****************************
+
+What's new
+----------
+
+Builder (/build):
+- 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
+
+.. Note::
+The /build improvements are not reverse-compatible. Pre 1.3 clients will break on /build.
+
+
 :doc:`docker_remote_api_v1.2`
 :doc:`docker_remote_api_v1.2`
 *****************************
 *****************************
 
 

+ 4 - 1
docs/sources/api/docker_remote_api_v1.2.rst

@@ -847,7 +847,7 @@ Build an image from Dockerfile via stdin
 
 
 .. http:post:: /build
 .. http:post:: /build
 
 
-	Build an image from Dockerfile via stdin
+	Build an image from Dockerfile
 
 
 	**Example request**:
 	**Example request**:
 
 
@@ -866,9 +866,12 @@ Build an image from Dockerfile via stdin
 	   {{ STREAM }}
 	   {{ STREAM }}
 
 
 	:query t: tag to be applied to the resulting image in case of success
 	:query t: tag to be applied to the resulting image in case of success
+	:query remote: resource to fetch, as URI
 	:statuscode 200: no error
 	:statuscode 200: no error
         :statuscode 500: server error
         :statuscode 500: server error
 
 
+{{ STREAM }} is the raw text output of the build command. It uses the HTTP Hijack method in order to stream.
+
 
 
 Check auth configuration
 Check auth configuration
 ************************
 ************************

+ 1038 - 0
docs/sources/api/docker_remote_api_v1.3.rst

@@ -0,0 +1,1038 @@
+:title: Remote API v1.3
+:description: API Documentation for Docker
+:keywords: API, Docker, rcli, REST, documentation
+
+======================
+Docker Remote API v1.3
+======================
+
+.. contents:: Table of Contents
+
+1. Brief introduction
+=====================
+
+- The Remote API is replacing rcli
+- Default port in the docker deamon is 4243 
+- The API tends to be REST, but for some complex commands, like attach or pull, the HTTP connection is hijacked to transport stdout stdin and stderr
+
+2. Endpoints
+============
+
+2.1 Containers
+--------------
+
+List containers
+***************
+
+.. http:get:: /containers/json
+
+	List containers
+
+	**Example request**:
+
+	.. sourcecode:: http
+
+	   GET /containers/json?all=1&before=8dfafdbc3a40 HTTP/1.1
+	   
+	**Example response**:
+
+	.. sourcecode:: http
+
+	   HTTP/1.1 200 OK
+	   Content-Type: application/json
+	   
+	   [
+		{
+			"Id": "8dfafdbc3a40",
+			"Image": "base:latest",
+			"Command": "echo 1",
+			"Created": 1367854155,
+			"Status": "Exit 0",
+			"Ports":"",
+			"SizeRw":12288,
+			"SizeRootFs":0
+		},
+		{
+			"Id": "9cd87474be90",
+			"Image": "base:latest",
+			"Command": "echo 222222",
+			"Created": 1367854155,
+			"Status": "Exit 0",
+			"Ports":"",
+			"SizeRw":12288,
+			"SizeRootFs":0
+		},
+		{
+			"Id": "3176a2479c92",
+			"Image": "base:latest",
+			"Command": "echo 3333333333333333",
+			"Created": 1367854154,
+			"Status": "Exit 0",
+			"Ports":"",
+			"SizeRw":12288,
+			"SizeRootFs":0
+		},
+		{
+			"Id": "4cb07b47f9fb",
+			"Image": "base:latest",
+			"Command": "echo 444444444444444444444444444444444",
+			"Created": 1367854152,
+			"Status": "Exit 0",
+			"Ports":"",
+			"SizeRw":12288,
+			"SizeRootFs":0
+		}
+	   ]
+ 
+	:query all: 1/True/true or 0/False/false, Show all containers. Only running containers are shown by default
+	:query limit: Show ``limit`` last created containers, include non-running ones.
+	:query since: Show only containers created since Id, include non-running ones.
+	:query before: Show only containers created before Id, include non-running ones.
+	:statuscode 200: no error
+	:statuscode 400: bad parameter
+	:statuscode 500: server error
+
+
+Create a container
+******************
+
+.. http:post:: /containers/create
+
+	Create a container
+
+	**Example request**:
+
+	.. sourcecode:: http
+
+	   POST /containers/create HTTP/1.1
+	   Content-Type: application/json
+
+	   {
+		"Hostname":"",
+		"User":"",
+		"Memory":0,
+		"MemorySwap":0,
+		"AttachStdin":false,
+		"AttachStdout":true,
+		"AttachStderr":true,
+		"PortSpecs":null,
+		"Tty":false,
+		"OpenStdin":false,
+		"StdinOnce":false,
+		"Env":null,
+		"Cmd":[
+			"date"
+		],
+		"Dns":null,
+		"Image":"base",
+		"Volumes":{},
+		"VolumesFrom":""
+	   }
+	   
+	**Example response**:
+
+	.. sourcecode:: http
+
+	   HTTP/1.1 201 OK
+	   Content-Type: application/json
+
+	   {
+		"Id":"e90e34656806"
+		"Warnings":[]
+	   }
+	
+	:jsonparam config: the container's configuration
+	:statuscode 201: no error
+	:statuscode 404: no such container
+	:statuscode 406: impossible to attach (container not running)
+	:statuscode 500: server error
+
+
+Inspect a container
+*******************
+
+.. http:get:: /containers/(id)/json
+
+	Return low-level information on the container ``id``
+
+	**Example request**:
+
+	.. sourcecode:: http
+
+	   GET /containers/4fa6e0f0c678/json HTTP/1.1
+	   
+	**Example response**:
+
+	.. sourcecode:: http
+
+	   HTTP/1.1 200 OK
+	   Content-Type: application/json
+
+	   {
+			"Id": "4fa6e0f0c6786287e131c3852c58a2e01cc697a68231826813597e4994f1d6e2",
+			"Created": "2013-05-07T14:51:42.041847+02:00",
+			"Path": "date",
+			"Args": [],
+			"Config": {
+				"Hostname": "4fa6e0f0c678",
+				"User": "",
+				"Memory": 0,
+				"MemorySwap": 0,
+				"AttachStdin": false,
+				"AttachStdout": true,
+				"AttachStderr": true,
+				"PortSpecs": null,
+				"Tty": false,
+				"OpenStdin": false,
+				"StdinOnce": false,
+				"Env": null,
+				"Cmd": [
+					"date"
+				],
+				"Dns": null,
+				"Image": "base",
+				"Volumes": {},
+				"VolumesFrom": ""
+			},
+			"State": {
+				"Running": false,
+				"Pid": 0,
+				"ExitCode": 0,
+				"StartedAt": "2013-05-07T14:51:42.087658+02:01360",
+				"Ghost": false
+			},
+			"Image": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc",
+			"NetworkSettings": {
+				"IpAddress": "",
+				"IpPrefixLen": 0,
+				"Gateway": "",
+				"Bridge": "",
+				"PortMapping": null
+			},
+			"SysInitPath": "/home/kitty/go/src/github.com/dotcloud/docker/bin/docker",
+			"ResolvConfPath": "/etc/resolv.conf",
+			"Volumes": {}
+	   }
+
+	:statuscode 200: no error
+	:statuscode 404: no such container
+	:statuscode 500: server error
+
+
+Inspect changes on a container's filesystem
+*******************************************
+
+.. http:get:: /containers/(id)/changes
+
+	Inspect changes on container ``id`` 's filesystem
+
+	**Example request**:
+
+	.. sourcecode:: http
+
+	   GET /containers/4fa6e0f0c678/changes HTTP/1.1
+
+	   
+	**Example response**:
+
+	.. sourcecode:: http
+
+	   HTTP/1.1 200 OK
+	   Content-Type: application/json
+	   
+	   [
+		{
+			"Path":"/dev",
+			"Kind":0
+		},
+		{
+			"Path":"/dev/kmsg",
+			"Kind":1
+		},
+		{
+			"Path":"/test",
+			"Kind":1
+		}
+	   ]
+
+	:statuscode 200: no error
+	:statuscode 404: no such container
+	:statuscode 500: server error
+
+
+Export a container
+******************
+
+.. http:get:: /containers/(id)/export
+
+	Export the contents of container ``id``
+
+	**Example request**:
+
+	.. sourcecode:: http
+
+	   GET /containers/4fa6e0f0c678/export HTTP/1.1
+
+	   
+	**Example response**:
+
+	.. sourcecode:: http
+
+	   HTTP/1.1 200 OK
+	   Content-Type: application/octet-stream
+	   
+	   {{ STREAM }}
+
+	:statuscode 200: no error
+	:statuscode 404: no such container
+	:statuscode 500: server error
+
+
+Start a container
+*****************
+
+.. http:post:: /containers/(id)/start
+
+	Start the container ``id``
+
+	**Example request**:
+
+	.. sourcecode:: http
+
+	   POST /containers/e90e34656806/start HTTP/1.1
+	   
+	**Example response**:
+
+	.. sourcecode:: http
+
+	   HTTP/1.1 200 OK
+	   	
+	:statuscode 200: no error
+	:statuscode 404: no such container
+	:statuscode 500: server error
+
+
+Stop a contaier
+***************
+
+.. http:post:: /containers/(id)/stop
+
+	Stop the container ``id``
+
+	**Example request**:
+
+	.. sourcecode:: http
+
+	   POST /containers/e90e34656806/stop?t=5 HTTP/1.1
+	   
+	**Example response**:
+
+	.. sourcecode:: http
+
+	   HTTP/1.1 204 OK
+	   	
+	:query t: number of seconds to wait before killing the container
+	:statuscode 204: no error
+	:statuscode 404: no such container
+	:statuscode 500: server error
+
+
+Restart a container
+*******************
+
+.. http:post:: /containers/(id)/restart
+
+	Restart the container ``id``
+
+	**Example request**:
+
+	.. sourcecode:: http
+
+	   POST /containers/e90e34656806/restart?t=5 HTTP/1.1
+	   
+	**Example response**:
+
+	.. sourcecode:: http
+
+	   HTTP/1.1 204 OK
+	   	
+	:query t: number of seconds to wait before killing the container
+	:statuscode 204: no error
+	:statuscode 404: no such container
+	:statuscode 500: server error
+
+
+Kill a container
+****************
+
+.. http:post:: /containers/(id)/kill
+
+	Kill the container ``id``
+
+	**Example request**:
+
+	.. sourcecode:: http
+
+	   POST /containers/e90e34656806/kill HTTP/1.1
+	   
+	**Example response**:
+
+	.. sourcecode:: http
+
+	   HTTP/1.1 204 OK
+	   	
+	:statuscode 204: no error
+	:statuscode 404: no such container
+	:statuscode 500: server error
+
+
+Attach to a container
+*********************
+
+.. http:post:: /containers/(id)/attach
+
+	Attach to the container ``id``
+
+	**Example request**:
+
+	.. sourcecode:: http
+
+	   POST /containers/16253994b7c4/attach?logs=1&stream=0&stdout=1 HTTP/1.1
+	   
+	**Example response**:
+
+	.. sourcecode:: http
+
+	   HTTP/1.1 200 OK
+	   Content-Type: application/vnd.docker.raw-stream
+
+	   {{ STREAM }}
+	   	
+	:query logs: 1/True/true or 0/False/false, return logs. Default false
+	:query stream: 1/True/true or 0/False/false, return stream. Default false
+	:query stdin: 1/True/true or 0/False/false, if stream=true, attach to stdin. Default false
+	:query stdout: 1/True/true or 0/False/false, if logs=true, return stdout log, if stream=true, attach to stdout. Default false
+	:query stderr: 1/True/true or 0/False/false, if logs=true, return stderr log, if stream=true, attach to stderr. Default false
+	:statuscode 200: no error
+	:statuscode 400: bad parameter
+	:statuscode 404: no such container
+	:statuscode 500: server error
+
+
+Wait a container
+****************
+
+.. http:post:: /containers/(id)/wait
+
+	Block until container ``id`` stops, then returns the exit code
+
+	**Example request**:
+
+	.. sourcecode:: http
+
+	   POST /containers/16253994b7c4/wait HTTP/1.1
+	   
+	**Example response**:
+
+	.. sourcecode:: http
+
+	   HTTP/1.1 200 OK
+	   Content-Type: application/json
+
+	   {"StatusCode":0}
+	   	
+	:statuscode 200: no error
+	:statuscode 404: no such container
+	:statuscode 500: server error
+
+
+Remove a container
+*******************
+
+.. http:delete:: /containers/(id)
+
+	Remove the container ``id`` from the filesystem
+
+	**Example request**:
+
+        .. sourcecode:: http
+
+           DELETE /containers/16253994b7c4?v=1 HTTP/1.1
+
+        **Example response**:
+
+        .. sourcecode:: http
+
+	   HTTP/1.1 204 OK
+
+	:query v: 1/True/true or 0/False/false, Remove the volumes associated to the container. Default false
+        :statuscode 204: no error
+	:statuscode 400: bad parameter
+        :statuscode 404: no such container
+        :statuscode 500: server error
+
+
+2.2 Images
+----------
+
+List Images
+***********
+
+.. http:get:: /images/(format)
+
+	List images ``format`` could be json or viz (json default)
+
+	**Example request**:
+
+	.. sourcecode:: http
+
+	   GET /images/json?all=0 HTTP/1.1
+
+	**Example response**:
+
+	.. sourcecode:: http
+
+	   HTTP/1.1 200 OK
+	   Content-Type: application/json
+	   
+	   [
+		{
+			"Repository":"base",
+			"Tag":"ubuntu-12.10",
+			"Id":"b750fe79269d",
+			"Created":1364102658,
+			"Size":24653,
+			"VirtualSize":180116135
+		},
+		{
+			"Repository":"base",
+			"Tag":"ubuntu-quantal",
+			"Id":"b750fe79269d",
+			"Created":1364102658,
+			"Size":24653,
+			"VirtualSize":180116135
+		}
+	   ]
+
+
+	**Example request**:
+
+	.. sourcecode:: http
+
+	   GET /images/viz HTTP/1.1
+
+	**Example response**:
+
+	.. sourcecode:: http
+
+	   HTTP/1.1 200 OK
+	   Content-Type: text/plain
+
+	   digraph docker {
+	   "d82cbacda43a" -> "074be284591f"
+	   "1496068ca813" -> "08306dc45919"
+	   "08306dc45919" -> "0e7893146ac2"
+	   "b750fe79269d" -> "1496068ca813"
+	   base -> "27cf78414709" [style=invis]
+	   "f71189fff3de" -> "9a33b36209ed"
+	   "27cf78414709" -> "b750fe79269d"
+	   "0e7893146ac2" -> "d6434d954665"
+	   "d6434d954665" -> "d82cbacda43a"
+	   base -> "e9aa60c60128" [style=invis]
+	   "074be284591f" -> "f71189fff3de"
+	   "b750fe79269d" [label="b750fe79269d\nbase",shape=box,fillcolor="paleturquoise",style="filled,rounded"];
+	   "e9aa60c60128" [label="e9aa60c60128\nbase2",shape=box,fillcolor="paleturquoise",style="filled,rounded"];
+	   "9a33b36209ed" [label="9a33b36209ed\ntest",shape=box,fillcolor="paleturquoise",style="filled,rounded"];
+	   base [style=invisible]
+	   }
+ 
+	:query all: 1/True/true or 0/False/false, Show all containers. Only running containers are shown by default
+	:statuscode 200: no error
+	:statuscode 400: bad parameter
+	:statuscode 500: server error
+
+
+Create an image
+***************
+
+.. http:post:: /images/create
+
+	Create an image, either by pull it from the registry or by importing it
+
+	**Example request**:
+
+        .. sourcecode:: http
+
+           POST /images/create?fromImage=base HTTP/1.1
+
+        **Example response**:
+
+        .. sourcecode:: http
+
+           HTTP/1.1 200 OK
+	   Content-Type: application/json
+
+	   {"status":"Pulling..."}
+	   {"status":"Pulling", "progress":"1/? (n/a)"}
+	   {"error":"Invalid..."}
+	   ...
+
+        :query fromImage: name of the image to pull
+	:query fromSrc: source to import, - means stdin
+        :query repo: repository
+	:query tag: tag
+	:query registry: the registry to pull from
+        :statuscode 200: no error
+        :statuscode 500: server error
+
+
+Insert a file in a image
+************************
+
+.. http:post:: /images/(name)/insert
+
+	Insert a file from ``url`` in the image ``name`` at ``path``
+
+	**Example request**:
+
+        .. sourcecode:: http
+
+           POST /images/test/insert?path=/usr&url=myurl HTTP/1.1
+
+	**Example response**:
+
+        .. sourcecode:: http
+
+           HTTP/1.1 200 OK
+	   Content-Type: application/json
+
+	   {"status":"Inserting..."}
+	   {"status":"Inserting", "progress":"1/? (n/a)"}
+	   {"error":"Invalid..."}
+	   ...
+
+	:statuscode 200: no error
+        :statuscode 500: server error
+
+
+Inspect an image
+****************
+
+.. http:get:: /images/(name)/json
+
+	Return low-level information on the image ``name``
+
+	**Example request**:
+
+	.. sourcecode:: http
+
+	   GET /images/base/json HTTP/1.1
+
+	**Example response**:
+
+        .. sourcecode:: http
+
+           HTTP/1.1 200 OK
+	   Content-Type: application/json
+
+	   {
+		"id":"b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc",
+		"parent":"27cf784147099545",
+		"created":"2013-03-23T22:24:18.818426-07:00",
+		"container":"3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0",
+		"container_config":
+			{
+				"Hostname":"",
+				"User":"",
+				"Memory":0,
+				"MemorySwap":0,
+				"AttachStdin":false,
+				"AttachStdout":false,
+				"AttachStderr":false,
+				"PortSpecs":null,
+				"Tty":true,
+				"OpenStdin":true,
+				"StdinOnce":false,
+				"Env":null,
+				"Cmd": ["/bin/bash"]
+				,"Dns":null,
+				"Image":"base",
+				"Volumes":null,
+				"VolumesFrom":""
+			},
+		"Size": 6824592
+	   }
+
+	:statuscode 200: no error
+	:statuscode 404: no such image
+        :statuscode 500: server error
+
+
+Get the history of an image
+***************************
+
+.. http:get:: /images/(name)/history
+
+        Return the history of the image ``name``
+
+        **Example request**:
+
+        .. sourcecode:: http
+
+           GET /images/base/history HTTP/1.1
+
+        **Example response**:
+
+        .. sourcecode:: http
+
+           HTTP/1.1 200 OK
+	   Content-Type: application/json
+
+	   [
+		{
+			"Id":"b750fe79269d",
+			"Created":1364102658,
+			"CreatedBy":"/bin/bash"
+		},
+		{
+			"Id":"27cf78414709",
+			"Created":1364068391,
+			"CreatedBy":""
+		}
+	   ]
+
+        :statuscode 200: no error
+        :statuscode 404: no such image
+        :statuscode 500: server error
+
+
+Push an image on the registry
+*****************************
+
+.. http:post:: /images/(name)/push
+
+	Push the image ``name`` on the registry
+
+	 **Example request**:
+
+	 .. sourcecode:: http
+
+	    POST /images/test/push HTTP/1.1
+	    {{ authConfig }}
+
+	 **Example response**:
+
+        .. sourcecode:: http
+
+           HTTP/1.1 200 OK
+	   Content-Type: application/json
+
+	   {"status":"Pushing..."}
+	   {"status":"Pushing", "progress":"1/? (n/a)"}
+	   {"error":"Invalid..."}
+	   ...
+
+	:query registry: the registry you wan to push, optional
+	:statuscode 200: no error
+        :statuscode 404: no such image
+        :statuscode 500: server error
+
+
+Tag an image into a repository
+******************************
+
+.. http:post:: /images/(name)/tag
+
+	Tag the image ``name`` into a repository
+
+        **Example request**:
+
+        .. sourcecode:: http
+			
+	   POST /images/test/tag?repo=myrepo&force=0 HTTP/1.1
+
+	**Example response**:
+
+        .. sourcecode:: http
+
+           HTTP/1.1 200 OK
+
+	:query repo: The repository to tag in
+	:query force: 1/True/true or 0/False/false, default false
+	:statuscode 200: no error
+	:statuscode 400: bad parameter
+	:statuscode 404: no such image
+	:statuscode 409: conflict
+        :statuscode 500: server error
+
+
+Remove an image
+***************
+
+.. http:delete:: /images/(name)
+
+	Remove the image ``name`` from the filesystem 
+	
+	**Example request**:
+
+	.. sourcecode:: http
+
+	   DELETE /images/test HTTP/1.1
+
+	**Example response**:
+
+        .. sourcecode:: http
+
+	   HTTP/1.1 200 OK
+	   Content-type: application/json
+
+	   [
+	    {"Untagged":"3e2f21a89f"},
+	    {"Deleted":"3e2f21a89f"},
+	    {"Deleted":"53b4f83ac9"}
+	   ]
+
+	:statuscode 204: no error
+        :statuscode 404: no such image
+	:statuscode 409: conflict
+        :statuscode 500: server error
+
+
+Search images
+*************
+
+.. http:get:: /images/search
+
+	Search for an image in the docker index
+	
+	**Example request**:
+
+        .. sourcecode:: http
+
+           GET /images/search?term=sshd HTTP/1.1
+
+	**Example response**:
+
+	.. sourcecode:: http
+
+	   HTTP/1.1 200 OK
+	   Content-Type: application/json
+	   
+	   [
+		{
+			"Name":"cespare/sshd",
+			"Description":""
+		},
+		{
+			"Name":"johnfuller/sshd",
+			"Description":""
+		},
+		{
+			"Name":"dhrp/mongodb-sshd",
+			"Description":""
+		}
+	   ]
+
+	   :query term: term to search
+	   :statuscode 200: no error
+	   :statuscode 500: server error
+
+
+2.3 Misc
+--------
+
+Build an image from Dockerfile via stdin
+****************************************
+
+.. http:post:: /build
+
+	Build an image from Dockerfile via stdin
+
+	**Example request**:
+
+        .. sourcecode:: http
+
+           POST /build HTTP/1.1
+	   
+	   {{ STREAM }}
+
+	**Example response**:
+
+        .. sourcecode:: http
+
+           HTTP/1.1 200 OK
+	   
+	   {{ STREAM }}
+
+
+        The stream must be a tar archive compressed with one of the following algorithms:
+        identity (no compression), gzip, bzip2, xz. The archive must include a file called
+        `Dockerfile` at its root. It may include any number of other files, which will be
+        accessible in the build context (See the ADD build command).
+
+        The Content-type header should be set to "application/tar".
+
+	:query t: tag to be applied to the resulting image in case of success
+	:statuscode 200: no error
+        :statuscode 500: server error
+
+
+Check auth configuration
+************************
+
+.. http:post:: /auth
+
+        Get the default username and email
+
+        **Example request**:
+
+        .. sourcecode:: http
+
+           POST /auth HTTP/1.1
+	   Content-Type: application/json
+
+	   {
+		"username":"hannibal",
+		"password:"xxxx",
+		"email":"hannibal@a-team.com"
+	   }
+
+        **Example response**:
+
+        .. sourcecode:: http
+
+           HTTP/1.1 200 OK
+
+        :statuscode 200: no error
+        :statuscode 204: no error
+        :statuscode 500: server error
+
+
+Display system-wide information
+*******************************
+
+.. http:get:: /info
+
+	Display system-wide information
+	
+	**Example request**:
+
+        .. sourcecode:: http
+
+           GET /info HTTP/1.1
+
+        **Example response**:
+
+        .. sourcecode:: http
+
+           HTTP/1.1 200 OK
+	   Content-Type: application/json
+
+	   {
+		"Containers":11,
+		"Images":16,
+		"Debug":false,
+		"NFd": 11,
+		"NGoroutines":21,
+		"MemoryLimit":true,
+		"SwapLimit":false
+	   }
+
+        :statuscode 200: no error
+        :statuscode 500: server error
+
+
+Show the docker version information
+***********************************
+
+.. http:get:: /version
+
+	Show the docker version information
+
+	**Example request**:
+
+        .. sourcecode:: http
+
+           GET /version HTTP/1.1
+
+        **Example response**:
+
+        .. sourcecode:: http
+
+           HTTP/1.1 200 OK
+	   Content-Type: application/json
+
+	   {
+		"Version":"0.2.2",
+		"GitCommit":"5a2a5cc+CHANGES",
+		"GoVersion":"go1.0.3"
+	   }
+
+        :statuscode 200: no error
+	:statuscode 500: server error
+
+
+Create a new image from a container's changes
+*********************************************
+
+.. http:post:: /commit
+
+	Create a new image from a container's changes
+
+	**Example request**:
+
+        .. sourcecode:: http
+
+           POST /commit?container=44c004db4b17&m=message&repo=myrepo HTTP/1.1
+
+        **Example response**:
+
+        .. sourcecode:: http
+
+           HTTP/1.1 201 OK
+	   Content-Type: application/vnd.docker.raw-stream
+
+           {"Id":"596069db4bf5"}
+
+	:query container: source container
+	:query repo: repository
+	:query tag: tag
+	:query m: commit message
+	:query author: author (eg. "John Hannibal Smith <hannibal@a-team.com>")
+	:query run: config automatically applied when the image is run. (ex: {"Cmd": ["cat", "/world"], "PortSpecs":["22"]})
+        :statuscode 201: no error
+	:statuscode 404: no such container
+        :statuscode 500: server error
+
+
+3. Going further
+================
+
+3.1 Inside 'docker run'
+-----------------------
+
+Here are the steps of 'docker run' :
+
+* Create the container
+* If the status code is 404, it means the image doesn't exists:
+        * Try to pull it
+        * Then retry to create the container
+* Start the container
+* If you are not in detached mode:
+        * Attach to the container, using logs=1 (to have stdout and stderr from the container's start) and stream=1
+* If in detached mode or only stdin is attached:
+	* Display the container's id
+
+
+3.2 Hijacking
+-------------
+
+In this version of the API, /attach, uses hijacking to transport stdin, stdout and stderr on the same socket. This might change in the future.
+
+3.3 CORS Requests
+-----------------
+
+To enable cross origin requests to the remote api add the flag "-api-enable-cors" when running docker in daemon mode.
+    
+    docker -d -H="192.168.1.9:4243" -api-enable-cors
+

+ 12 - 2
docs/sources/commandline/command/build.rst

@@ -8,9 +8,11 @@
 
 
 ::
 ::
 
 
-    Usage: docker build [OPTIONS] PATH | -
+    Usage: docker build [OPTIONS] PATH | URL | -
     Build a new container image from the source code at PATH
     Build a new container image from the source code at PATH
       -t="": Tag to be applied to the resulting image in case of success.
       -t="": Tag to be applied to the resulting image in case of success.
+    When a single Dockerfile is given as URL, then no context is set. When a git repository is set as URL, the repository is used as context
+
 
 
 Examples
 Examples
 --------
 --------
@@ -27,7 +29,15 @@ Examples
 
 
 .. code-block:: bash
 .. code-block:: bash
 
 
-    docker build -
+    docker build - < Dockerfile
 
 
 | This will read a Dockerfile from Stdin without context. Due to the lack of a context, no contents of any local directory will be sent to the docker daemon.
 | This will read a Dockerfile from Stdin without context. Due to the lack of a context, no contents of any local directory will be sent to the docker daemon.
 | ADD doesn't work when running in this mode due to the absence of the context, thus having no source files to copy to the container.
 | ADD doesn't work when running in this mode due to the absence of the context, thus having no source files to copy to the container.
+
+
+.. code-block:: bash
+
+    docker build github.com/creack/docker-firefox
+
+| This will clone the github repository and use it as context. The Dockerfile at the root of the repository is used as Dockerfile.
+| Note that you can specify an arbitrary git repository by using the 'git://' schema.

+ 2 - 15
docs/sources/use/builder.rst

@@ -121,19 +121,7 @@ functionally equivalent to prefixing the command with `<key>=<value>`
 .. note::
 .. note::
     The environment variables will persist when a container is run from the resulting image.
     The environment variables will persist when a container is run from the resulting image.
 
 
-2.7 INSERT
-----------
-
-    ``INSERT <file url> <path>``
-
-The `INSERT` instruction will download the file from the given url to the given
-path within the image. It is similar to `RUN curl -o <path> <url>`, assuming 
-curl was installed within the image.
-
-.. note::
-    The path must include the file name.
-
-2.8 ADD
+2.7 ADD
 -------
 -------
 
 
     ``ADD <src> <dest>``
     ``ADD <src> <dest>``
@@ -141,7 +129,7 @@ curl was installed within the image.
 The `ADD` instruction will copy new files from <src> and add them to the container's filesystem at path `<dest>`.
 The `ADD` instruction will copy new files from <src> and add them to the container's filesystem at path `<dest>`.
 
 
 `<src>` must be the path to a file or directory relative to the source directory being built (also called the
 `<src>` must be the path to a file or directory relative to the source directory being built (also called the
-context of the build).
+context of the build) or a remote file URL.
 
 
 `<dest>` is the path at which the source will be copied in the destination container.
 `<dest>` is the path at which the source will be copied in the destination container.
 
 
@@ -182,7 +170,6 @@ files and directories are created with mode 0700, uid and gid 0.
     RUN apt-get update
     RUN apt-get update
     
     
     RUN apt-get install -y inotify-tools nginx apache2 openssh-server
     RUN apt-get install -y inotify-tools nginx apache2 openssh-server
-    INSERT https://raw.github.com/creack/docker-vps/master/nginx-wrapper.sh /usr/sbin/nginx-wrapper
 
 
 .. code-block:: bash
 .. code-block:: bash
 
 

+ 1 - 1
registry/registry.go

@@ -314,7 +314,7 @@ func (r *Registry) opaqueRequest(method, urlStr string, body io.Reader) (*http.R
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
-	req.URL.Opaque = strings.Replace(urlStr, req.URL.Scheme + ":", "", 1)
+	req.URL.Opaque = strings.Replace(urlStr, req.URL.Scheme+":", "", 1)
 	return req, err
 	return req, err
 }
 }
 
 

+ 8 - 0
utils/utils.go

@@ -636,6 +636,14 @@ func (sf *StreamFormatter) Used() bool {
 	return sf.used
 	return sf.used
 }
 }
 
 
+func IsURL(str string) bool {
+	return strings.HasPrefix(str, "http://") || strings.HasPrefix(str, "https://")
+}
+
+func IsGIT(str string) bool {
+	return strings.HasPrefix(str, "git://") || strings.HasPrefix(str, "github.com/")
+}
+
 func CheckLocalDns() bool {
 func CheckLocalDns() bool {
 	resolv, err := ioutil.ReadFile("/etc/resolv.conf")
 	resolv, err := ioutil.ReadFile("/etc/resolv.conf")
 	if err != nil {
 	if err != nil {