diff --git a/api.go b/api.go index 941e87e6f0..264874373f 100644 --- a/api.go +++ b/api.go @@ -33,6 +33,13 @@ func parseForm(r *http.Request) error { return nil } +func parseMultipartForm(r *http.Request) error { + if err := r.ParseMultipartForm(4096); err != nil && !strings.HasPrefix(err.Error(), "mime:") { + return err + } + return nil +} + func httpError(w http.ResponseWriter, err error) { if strings.HasPrefix(err.Error(), "No such") { http.Error(w, err.Error(), http.StatusNotFound) @@ -335,9 +342,15 @@ func postImagesInsert(srv *Server, version float64, w http.ResponseWriter, r *ht } name := vars["name"] - if err := srv.ImageInsert(name, url, path, w); err != nil { + imgId, err := srv.ImageInsert(name, url, path, w) + if err != nil { return err } + b, err := json.Marshal(&ApiId{Id: imgId}) + if err != nil { + return err + } + writeJson(w, b) return nil } @@ -617,6 +630,30 @@ func postImagesGetCache(srv *Server, version float64, w http.ResponseWriter, r * return nil } +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 + } + + 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 _, err := b.Build(dockerfile, context); err != nil { + fmt.Fprintf(w, "Error build: %s\n", err) + } + return nil +} + func ListenAndServe(addr string, srv *Server, logging bool) error { r := mux.NewRouter() log.Printf("Listening for HTTP on %s\n", addr) @@ -640,6 +677,7 @@ func ListenAndServe(addr string, srv *Server, logging bool) error { "POST": { "/auth": postAuth, "/commit": postCommit, + "/build": postBuild, "/images/create": postImagesCreate, "/images/{name:.*}/insert": postImagesInsert, "/images/{name:.*}/push": postImagesPush, diff --git a/archive.go b/archive.go index 8a011eb6e1..4120a52c1d 100644 --- a/archive.go +++ b/archive.go @@ -2,6 +2,7 @@ package docker import ( "errors" + "fmt" "io" "io/ioutil" "os" @@ -31,6 +32,20 @@ func (compression *Compression) Flag() string { return "" } +func (compression *Compression) Extension() string { + switch *compression { + case Uncompressed: + return "tar" + case Bzip2: + return "tar.bz2" + case Gzip: + return "tar.gz" + case Xz: + return "tar.xz" + } + return "" +} + func Tar(path string, compression Compression) (io.Reader, error) { cmd := exec.Command("bsdtar", "-f", "-", "-C", path, "-c"+compression.Flag(), ".") return CmdStream(cmd) @@ -41,7 +56,7 @@ func Untar(archive io.Reader, path string) error { cmd.Stdin = archive output, err := cmd.CombinedOutput() if err != nil { - return errors.New(err.Error() + ": " + string(output)) + return fmt.Errorf("%s: %s", err, output) } return nil } diff --git a/builder_client.go b/builder_client.go index c3950b2be3..144e11b415 100644 --- a/builder_client.go +++ b/builder_client.go @@ -12,12 +12,6 @@ import ( "strings" ) -type BuilderClient interface { - Build(io.Reader) (string, error) - CmdFrom(string) error - CmdRun(string) error -} - type builderClient struct { cli *DockerCli @@ -158,8 +152,23 @@ func (b *builderClient) CmdExpose(args string) error { } func (b *builderClient) CmdInsert(args string) error { - // FIXME: Reimplement this once the remove_hijack branch gets merged. - // We need to retrieve the resulting Id + // 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") } @@ -240,7 +249,7 @@ func (b *builderClient) commit(id string) error { return nil } -func (b *builderClient) Build(dockerfile io.Reader) (string, error) { +func (b *builderClient) Build(dockerfile, context io.Reader) (string, error) { defer b.clearTmp(b.tmpContainers, b.tmpImages) file := bufio.NewReader(dockerfile) for { @@ -263,18 +272,18 @@ func (b *builderClient) Build(dockerfile io.Reader) (string, error) { instruction := strings.ToLower(strings.Trim(tmp[0], " ")) arguments := strings.Trim(tmp[1], " ") - fmt.Printf("%s %s (%s)\n", strings.ToUpper(instruction), arguments, b.image) + 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.Printf("Skipping unknown instruction %s\n", strings.ToUpper(instruction)) + 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.Printf("===> %v\n", b.image) + fmt.Fprintf(os.Stderr, "===> %v\n", b.image) } if b.needCommit { if err := b.commit(""); err != nil { @@ -289,13 +298,13 @@ func (b *builderClient) Build(dockerfile io.Reader) (string, error) { for i := range b.tmpContainers { delete(b.tmpContainers, i) } - fmt.Printf("Build finished. image id: %s\n", b.image) + 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(addr string, port int) BuilderClient { +func NewBuilderClient(addr string, port int) BuildFile { return &builderClient{ cli: NewDockerCli(addr, port), config: &Config{}, diff --git a/buildfile.go b/buildfile.go new file mode 100644 index 0000000000..23f2f47172 --- /dev/null +++ b/buildfile.go @@ -0,0 +1,377 @@ +package docker + +import ( + "bufio" + "encoding/json" + "fmt" + "github.com/dotcloud/docker/utils" + "io" + "io/ioutil" + "os" + "path" + "reflect" + "strings" +) + +type BuildFile interface { + Build(io.Reader, io.Reader) (string, error) + CmdFrom(string) error + CmdRun(string) error +} + +type buildFile struct { + runtime *Runtime + builder *Builder + srv *Server + + image string + maintainer string + config *Config + context string + + tmpContainers map[string]struct{} + tmpImages map[string]struct{} + + needCommit bool + + out io.Writer +} + +func (b *buildFile) clearTmp(containers, images map[string]struct{}) { + for c := range containers { + tmp := b.runtime.Get(c) + b.runtime.Destroy(tmp) + utils.Debugf("Removing container %s", c) + } + for i := range images { + b.runtime.graph.Delete(i) + utils.Debugf("Removing image %s", i) + } +} + +func (b *buildFile) CmdFrom(name string) error { + image, err := b.runtime.repositories.LookupImage(name) + if err != nil { + if b.runtime.graph.IsNotExist(err) { + + var tag, remote string + if strings.Contains(name, ":") { + remoteParts := strings.Split(name, ":") + tag = remoteParts[1] + remote = remoteParts[0] + } else { + remote = name + } + + if err := b.srv.ImagePull(remote, tag, "", b.out, false); err != nil { + return err + } + + image, err = b.runtime.repositories.LookupImage(name) + if err != nil { + return err + } + } else { + return err + } + } + b.image = image.Id + b.config = &Config{} + return nil +} + +func (b *buildFile) CmdMaintainer(name string) error { + b.needCommit = true + b.maintainer = name + return nil +} + +func (b *buildFile) 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) + + if cache, err := b.srv.ImageGetCached(b.image, config); err != nil { + return err + } else if cache != nil { + utils.Debugf("Use cached version") + b.image = cache.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 *buildFile) 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 *buildFile) 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 *buildFile) CmdExpose(args string) error { + ports := strings.Split(args, " ") + b.config.PortSpecs = append(ports, b.config.PortSpecs...) + return nil +} + +func (b *buildFile) CmdInsert(args string) error { + if b.image == "" { + return fmt.Errorf("Please provide a source image with `from` prior to insert") + } + tmp := strings.SplitN(args, " ", 2) + if len(tmp) != 2 { + return fmt.Errorf("Invalid INSERT format") + } + sourceUrl := strings.Trim(tmp[0], " ") + destPath := strings.Trim(tmp[1], " ") + + file, err := utils.Download(sourceUrl, b.out) + if err != nil { + return err + } + defer file.Body.Close() + + b.config.Cmd = []string{"echo", "INSERT", sourceUrl, "in", destPath} + cid, err := b.run() + if err != nil { + return err + } + + container := b.runtime.Get(cid) + if container == nil { + return fmt.Errorf("An error occured while creating the container") + } + + if err := container.Inject(file.Body, destPath); err != nil { + return err + } + + return b.commit(cid) +} + +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 INSERT format") + } + orig := strings.Trim(tmp[0], " ") + dest := strings.Trim(tmp[1], " ") + + b.config.Cmd = []string{"echo", "PUSH", orig, "in", dest} + cid, err := b.run() + if err != nil { + return err + } + + container := b.runtime.Get(cid) + if container == nil { + return fmt.Errorf("Error while creating the container (CmdAdd)") + } + + if err := os.MkdirAll(path.Join(container.rwPath(), dest), 0700); err != nil { + return err + } + + origPath := path.Join(b.context, orig) + destPath := path.Join(container.rwPath(), dest) + + fi, err := os.Stat(origPath) + if err != nil { + return err + } + if fi.IsDir() { + files, err := ioutil.ReadDir(path.Join(b.context, orig)) + if err != nil { + return err + } + for _, fi := range files { + if err := utils.CopyDirectory(path.Join(origPath, fi.Name()), path.Join(destPath, fi.Name())); err != nil { + return err + } + } + } else { + if err := utils.CopyDirectory(origPath, destPath); err != nil { + return err + } + } + + return b.commit(cid) +} + +func (b *buildFile) run() (string, error) { + if b.image == "" { + return "", fmt.Errorf("Please provide a source image with `from` prior to run") + } + b.config.Image = b.image + + // Create the container and start it + c, err := b.builder.Create(b.config) + if err != nil { + return "", err + } + b.tmpContainers[c.Id] = struct{}{} + + //start the container + if err := c.Start(); err != nil { + return "", err + } + + // Wait for it to finish + if ret := c.Wait(); ret != 0 { + return "", fmt.Errorf("The command %v returned a non-zero code: %d", b.config.Cmd, ret) + } + + return c.Id, nil +} + +func (b *buildFile) commit(id string) error { + if b.image == "" { + return fmt.Errorf("Please provide a source image with `from` prior to commit") + } + b.config.Image = b.image + if id == "" { + cmd := b.config.Cmd + b.config.Cmd = []string{"true"} + if cid, err := b.run(); err != nil { + return err + } else { + id = cid + } + b.config.Cmd = cmd + } + + container := b.runtime.Get(id) + if container == nil { + return fmt.Errorf("An error occured while creating the container") + } + + // Commit the container + image, err := b.builder.Commit(container, "", "", "", b.maintainer, nil) + if err != nil { + return err + } + b.tmpImages[image.Id] = struct{}{} + b.image = image.Id + b.needCommit = false + return nil +} + +func (b *buildFile) Build(dockerfile, context io.Reader) (string, error) { + defer b.clearTmp(b.tmpContainers, b.tmpImages) + + 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 + } + 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(b.out, "%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(b.out, "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(b.out, "===> %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) + } + fmt.Fprintf(b.out, "Build success.\n Image id:\n%s\n", b.image) + return b.image, nil + } + for i := range b.tmpContainers { + delete(b.tmpContainers, i) + } + return "", fmt.Errorf("An error occured during the build\n") +} + +func NewBuildFile(srv *Server, out io.Writer) BuildFile { + return &buildFile{ + builder: NewBuilder(srv.runtime), + runtime: srv.runtime, + srv: srv, + config: &Config{}, + out: out, + tmpContainers: make(map[string]struct{}), + tmpImages: make(map[string]struct{}), + } +} diff --git a/buildfile_test.go b/buildfile_test.go new file mode 100644 index 0000000000..b6f4e62ae9 --- /dev/null +++ b/buildfile_test.go @@ -0,0 +1,72 @@ +package docker + +import ( + "github.com/dotcloud/docker/utils" + "strings" + "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 +` + +func TestBuild(t *testing.T) { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + defer nuke(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 { + 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") + } +} diff --git a/commands.go b/commands.go index e3c28750e9..2af7e548fc 100644 --- a/commands.go +++ b/commands.go @@ -10,6 +10,7 @@ import ( "github.com/dotcloud/docker/utils" "io" "io/ioutil" + "mime/multipart" "net" "net/http" "net/http/httputil" @@ -74,7 +75,7 @@ func (cli *DockerCli) CmdHelp(args ...string) error { help := fmt.Sprintf("Usage: docker [OPTIONS] COMMAND [arg...]\n -H=\"%s:%d\": Host:port to bind/connect to\n\nA self-sufficient runtime for linux containers.\n\nCommands:\n", cli.host, cli.port) for cmd, description := range map[string]string{ "attach": "Attach to a running container", - "build": "Build a container from Dockerfile or via stdin", + "build": "Build a container from a Dockerfile", "commit": "Create a new image from a container's changes", "diff": "Inspect changes on a container's filesystem", "export": "Stream the contents of a container as a tar archive", @@ -122,39 +123,103 @@ func (cli *DockerCli) CmdInsert(args ...string) error { v.Set("url", cmd.Arg(1)) v.Set("path", cmd.Arg(2)) - err := cli.stream("POST", "/images/"+cmd.Arg(0)+"/insert?"+v.Encode(), nil, os.Stdout) - if err != nil { + if err := cli.stream("POST", "/images/"+cmd.Arg(0)+"/insert?"+v.Encode(), nil, os.Stdout); err != nil { return err } return nil } func (cli *DockerCli) CmdBuild(args ...string) error { - cmd := Subcmd("build", "-|Dockerfile", "Build an image from Dockerfile or via stdin") + cmd := Subcmd("build", "[OPTIONS] [CONTEXT]", "Build an image from a Dockerfile") + fileName := cmd.String("f", "Dockerfile", "Use `file` as Dockerfile. Can be '-' for stdin") if err := cmd.Parse(args); err != nil { return nil } + var ( - file io.ReadCloser - err error + file io.ReadCloser + multipartBody io.Reader + err error ) - if cmd.NArg() == 0 { - file, err = os.Open("Dockerfile") - if err != nil { - return err - } - } else if cmd.Arg(0) == "-" { + // 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") + + // Create a FormFile multipart for the Dockerfile + if *fileName == "-" { file = os.Stdin } else { - file, err = os.Open(cmd.Arg(0)) + file, err = os.Open(*fileName) if err != nil { return err } + defer file.Close() } - if _, err := NewBuilderClient("0.0.0.0", 4243).Build(file); err != nil { + if wField, err := w.CreateFormFile("Dockerfile", *fileName); err != nil { + return err + } else { + io.Copy(wField, file) + } + multipartBody = io.MultiReader(multipartBody, boundary) + + compression := Bzip2 + + // Create a FormFile multipart for the context if needed + if cmd.Arg(0) != "" { + // 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 + } + if wField, err := w.CreateFormFile("Context", filepath.Base(absPath)+"."+compression.Extension()); err != nil { + return err + } else { + // FIXME: Find a way to have a progressbar for the upload too + io.Copy(wField, utils.ProgressReader(ioutil.NopCloser(context), -1, os.Stdout, "Caching Context %v/%v (%v)\r", false)) + } + + multipartBody = io.MultiReader(multipartBody, boundary) + } + + // Send the multipart request with correct content-type + req, err := http.NewRequest("POST", fmt.Sprintf("http://%s:%d%s", cli.host, cli.port, "/build"), multipartBody) + if err != nil { return err } + req.Header.Set("Content-Type", w.FormDataContentType()) + if cmd.Arg(0) != "" { + req.Header.Set("X-Docker-Context-Compression", compression.Flag()) + fmt.Println("Uploading Context...") + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + // Check for errors + if resp.StatusCode < 200 || resp.StatusCode >= 400 { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + return fmt.Errorf("error: %s", body) + } + + // Output the result + if _, err := io.Copy(os.Stdout, resp.Body); err != nil { + return err + } + return nil } diff --git a/runtime_test.go b/runtime_test.go index 6c4ec5ded4..55671e12b6 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -8,7 +8,6 @@ import ( "io/ioutil" "net" "os" - "os/exec" "os/user" "sync" "testing" @@ -32,13 +31,6 @@ func nuke(runtime *Runtime) error { return os.RemoveAll(runtime.root) } -func CopyDirectory(source, dest string) error { - if _, err := exec.Command("cp", "-ra", source, dest).Output(); err != nil { - return err - } - return nil -} - func layerArchive(tarfile string) (io.Reader, error) { // FIXME: need to close f somewhere f, err := os.Open(tarfile) @@ -90,7 +82,7 @@ func newTestRuntime() (*Runtime, error) { if err := os.Remove(root); err != nil { return nil, err } - if err := CopyDirectory(unitTestStoreBase, root); err != nil { + if err := utils.CopyDirectory(unitTestStoreBase, root); err != nil { return nil, err } @@ -347,7 +339,7 @@ func TestRestore(t *testing.T) { if err := os.Remove(root); err != nil { t.Fatal(err) } - if err := CopyDirectory(unitTestStoreBase, root); err != nil { + if err := utils.CopyDirectory(unitTestStoreBase, root); err != nil { t.Fatal(err) } diff --git a/server.go b/server.go index 3303c7c5a1..2455587808 100644 --- a/server.go +++ b/server.go @@ -67,40 +67,40 @@ func (srv *Server) ImagesSearch(term string) ([]ApiSearch, error) { return outs, nil } -func (srv *Server) ImageInsert(name, url, path string, out io.Writer) error { +func (srv *Server) ImageInsert(name, url, path string, out io.Writer) (string, error) { out = utils.NewWriteFlusher(out) img, err := srv.runtime.repositories.LookupImage(name) if err != nil { - return err + return "", err } file, err := utils.Download(url, out) if err != nil { - return err + return "", err } defer file.Body.Close() config, _, err := ParseRun([]string{img.Id, "echo", "insert", url, path}, srv.runtime.capabilities) if err != nil { - return err + return "", err } b := NewBuilder(srv.runtime) c, err := b.Create(config) if err != nil { - return err + return "", err } if err := c.Inject(utils.ProgressReader(file.Body, int(file.ContentLength), out, "Downloading %v/%v (%v)\r", false), path); err != nil { - return err + return "", err } // FIXME: Handle custom repo, tag comment, author img, err = b.Commit(c, "", "", img.Comment, img.Author, nil) if err != nil { - return err + return "", err } fmt.Fprintf(out, "%s\n", img.Id) - return nil + return img.ShortId(), nil } func (srv *Server) ImagesViz(out io.Writer) error { diff --git a/utils/utils.go b/utils/utils.go index 233c624b68..97bdea9e9a 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -69,7 +69,7 @@ type progressReader struct { readProgress int // How much has been read so far (bytes) lastUpdate int // How many bytes read at least update template string // Template to print. Default "%v/%v (%v)" - json bool + json bool } func (r *progressReader) Read(p []byte) (n int, err error) { @@ -102,7 +102,7 @@ func (r *progressReader) Close() error { return io.ReadCloser(r.reader).Close() } func ProgressReader(r io.ReadCloser, size int, output io.Writer, template string, json bool) *progressReader { - if template == "" { + if template == "" { template = "%v/%v (%v)\r" } return &progressReader{r, NewWriteFlusher(output), size, 0, 0, template, json} @@ -532,6 +532,13 @@ func GetKernelVersion() (*KernelVersionInfo, error) { }, nil } +func CopyDirectory(source, dest string) error { + if output, err := exec.Command("cp", "-ra", source, dest).CombinedOutput(); err != nil { + return fmt.Errorf("Error copy: %s (%s)", err, output) + } + return nil +} + type NopFlusher struct{} func (f *NopFlusher) Flush() {} @@ -570,5 +577,3 @@ func FormatProgress(str string, json bool) string { } return "Downloading " + str + "\r" } - -