diff --git a/components/engine/api.go b/components/engine/api.go index 93322fd8aa..cf46ee316e 100644 --- a/components/engine/api.go +++ b/components/engine/api.go @@ -534,6 +534,23 @@ func postImagesPush(srv *Server, version float64, w http.ResponseWriter, r *http return nil } +func getImagesGet(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + name := vars["name"] + err := srv.ImageExport(name, w) + if err != nil { + return err + } + return nil +} + +func postImagesLoad(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + err := srv.ImageLoad(r.Body) + if err != nil { + return err + } + return nil +} + func postContainersCreate(srv *Server, version float64, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := parseForm(r); err != nil { return nil @@ -1036,6 +1053,7 @@ func createRouter(srv *Server, logging bool) (*mux.Router, error) { "/images/json": getImagesJSON, "/images/viz": getImagesViz, "/images/search": getImagesSearch, + "/images/{name:.*}/get": getImagesGet, "/images/{name:.*}/history": getImagesHistory, "/images/{name:.*}/json": getImagesByName, "/containers/ps": getContainersJSON, @@ -1052,6 +1070,7 @@ func createRouter(srv *Server, logging bool) (*mux.Router, error) { "/build": postBuild, "/images/create": postImagesCreate, "/images/{name:.*}/insert": postImagesInsert, + "/images/load": postImagesLoad, "/images/{name:.*}/push": postImagesPush, "/images/{name:.*}/tag": postImagesTag, "/containers/create": postContainersCreate, diff --git a/components/engine/commands.go b/components/engine/commands.go index 889aa72ffb..736334e40b 100644 --- a/components/engine/commands.go +++ b/components/engine/commands.go @@ -92,6 +92,7 @@ func (cli *DockerCli) CmdHelp(args ...string) error { {"insert", "Insert a file in an image"}, {"inspect", "Return low-level information on a container"}, {"kill", "Kill a running container"}, + {"load", "Load an image from a tar archive"}, {"login", "Register or Login to the docker registry server"}, {"logs", "Fetch the logs of a container"}, {"port", "Lookup the public-facing port which is NAT-ed to PRIVATE_PORT"}, @@ -102,6 +103,7 @@ func (cli *DockerCli) CmdHelp(args ...string) error { {"rm", "Remove one or more containers"}, {"rmi", "Remove one or more images"}, {"run", "Run a command in a new container"}, + {"save", "Save an image to a tar archive"}, {"search", "Search for an image in the docker index"}, {"start", "Start a stopped container"}, {"stop", "Stop a running container"}, @@ -1961,6 +1963,42 @@ func (cli *DockerCli) CmdCp(args ...string) error { return nil } +func (cli *DockerCli) CmdSave(args ...string) error { + cmd := Subcmd("save", "IMAGE DESTINATION", "Save an image to a tar archive") + if err := cmd.Parse(args); err != nil { + cmd.Usage() + return nil + } + + if cmd.NArg() != 1 { + cmd.Usage() + return nil + } + + image := cmd.Arg(0) + + if err := cli.stream("GET", "/images/"+image+"/get", nil, cli.out, nil); err != nil { + return err + } + return nil +} + +func (cli *DockerCli) CmdLoad(args ...string) error { + cmd := Subcmd("load", "SOURCE", "Load an image from a tar archive") + + if cmd.NArg() != 0 { + cmd.Usage() + return nil + } + + err := cli.stream("POST", "/images/load", cli.in, cli.out, nil) + if err != nil { + fmt.Println("Send failed", err) + } + + return nil +} + func (cli *DockerCli) call(method, path string, data interface{}) ([]byte, int, error) { var params io.Reader if data != nil { diff --git a/components/engine/docs/sources/commandline/cli.rst b/components/engine/docs/sources/commandline/cli.rst index d0a8d83c0c..2adfe48f90 100644 --- a/components/engine/docs/sources/commandline/cli.rst +++ b/components/engine/docs/sources/commandline/cli.rst @@ -559,6 +559,17 @@ Known Issues (kill) * :issue:`197` indicates that ``docker kill`` may leave directories behind and make it difficult to remove the container. +.. _cli_load: + +``load`` +-------- + +:: + Usage: docker load < repository.tar + + Loads a tarred repository from the standard input stream. + Restores both images and tags. + .. _cli_login: ``login`` @@ -852,6 +863,17 @@ Known Issues (run -volumes-from) could indicate a permissions problem with AppArmor. Please see the issue for a workaround. +.. _cli_save: + +``save`` + +:: + + Usage: docker save image > repository.tar + + Streams a tarred repository to the standard output stream. + Contains all parent layers, and all tags + versions. + .. _cli_search: ``search`` diff --git a/components/engine/server.go b/components/engine/server.go index e9a76f8d84..ac6401b2da 100644 --- a/components/engine/server.go +++ b/components/engine/server.go @@ -197,6 +197,155 @@ func (srv *Server) ContainerExport(name string, out io.Writer) error { return fmt.Errorf("No such container: %s", name) } +// ImageExport exports all images with the given tag. All versions +// containing the same tag are exported. The resulting output is an +// uncompressed tar ball. +// name is the set of tags to export. +// out is the writer where the images are written to. +func (srv *Server) ImageExport(name string, out io.Writer) error { + // get image json + tempdir, err := ioutil.TempDir("", "docker-export-") + if err != nil { + utils.Debugf("save", name, "") + return err + } + utils.Debugf("Serializing %s", name) + + rootRepo := srv.runtime.repositories.Repositories[name] + for _, rootImage := range rootRepo { + image, _ := srv.ImageInspect(rootImage) + for i := image; i != nil; { + // temporary directory + tmpImageDir := path.Join(tempdir, i.ID) + os.Mkdir(tmpImageDir, os.ModeDir) + + // serialize json + b, err := json.Marshal(i) + if err != nil { + utils.Debugf("%s", err) + os.RemoveAll(tempdir) + return err + } + ioutil.WriteFile(path.Join(tmpImageDir, "json"), b, os.ModeAppend) + + // serialize filesystem + fs, err := Tar(path.Join(srv.runtime.graph.Root, i.ID, "layer"), Uncompressed) + if err != nil { + utils.Debugf("%s", err) + os.RemoveAll(tempdir) + return err + } + fsTar, err := os.Create(path.Join(tmpImageDir, "layer.tar")) + if err != nil { + os.RemoveAll(tempdir) + utils.Debugf("%s", err) + return err + } + _, err = io.Copy(fsTar, fs) + if err != nil { + utils.Debugf("%s", err) + os.RemoveAll(tempdir) + return err + } + fsTar.Close() + + // find parent + if i.Parent != "" { + i, err = srv.ImageInspect(i.Parent) + if err != nil { + utils.Debugf("%s", err) + os.RemoveAll(tempdir) + return err + } + } else { + i = nil + } + } + } + + // write repositories + rootRepoMap := map[string]Repository{} + rootRepoMap[name] = rootRepo + rootRepoJson, _ := json.Marshal(rootRepoMap) + + ioutil.WriteFile(path.Join(tempdir, "repositories"), rootRepoJson, os.ModeAppend) + + fs, err := Tar(tempdir, Uncompressed) + if err != nil { + os.RemoveAll(tempdir) + return err + } + if _, err := io.Copy(out, fs); err != nil { + os.RemoveAll(tempdir) + return err + } + os.RemoveAll(tempdir) + return nil +} + +// Loads a set of images into the repository. This is the complementary of ImageExport. +// The input stream is an uncompressed tar ball containing images and metadata. +func (srv *Server) ImageLoad(in io.Reader) error { + tmpImageDir, _ := ioutil.TempDir("", "docker-import-") + repoTarFile := path.Join(tmpImageDir, "repo.tar") + repoDir := path.Join(tmpImageDir, "repo") + tarFile, _ := os.Create(repoTarFile) + io.Copy(tarFile, in) + tarFile.Close() + repoFile, _ := os.Open(repoTarFile) + os.Mkdir(repoDir, os.ModeDir) + Untar(repoFile, repoDir) + repositoriesJson, _ := ioutil.ReadFile(path.Join(tmpImageDir, "repo", "repositories")) + repositories := map[string]Repository{} + json.Unmarshal(repositoriesJson, &repositories) + + for imageName, tagMap := range repositories { + for tag, address := range tagMap { + err := srv.recursiveLoad(address, tmpImageDir) + if err != nil { + utils.Debugf("Error loading repository") + } + srv.runtime.repositories.Set(imageName, tag, address, true) + } + } + os.RemoveAll(tmpImageDir) + return nil +} + +func (srv *Server) recursiveLoad(address, tmpImageDir string) error { + _, err := srv.ImageInspect(address) + utils.Debugf("Attempting to load %s", "address") + if err != nil { + utils.Debugf("Loading %s", address) + imageJson, err := ioutil.ReadFile(path.Join(tmpImageDir, "repo", address, "json")) + if err != nil { + return err + utils.Debugf("Error reading json", err) + } + layer, err := os.Open(path.Join(tmpImageDir, "repo", address, "layer.tar")) + if err != nil { + utils.Debugf("Error reading embedded tar", err) + return err + } + img, err := NewImgJSON(imageJson) + if err != nil { + utils.Debugf("Error unmarshalling json", err) + return err + } + if img.Parent != "" { + if !srv.runtime.graph.Exists(img.Parent) { + srv.recursiveLoad(img.Parent, tmpImageDir) + } + } + err = srv.runtime.graph.Register(imageJson, layer, img) + if err != nil { + utils.Debugf("Error registering image") + } + } + utils.Debugf("Completed processing %s", address) + return nil +} + func (srv *Server) ImagesSearch(term string) ([]registry.SearchResult, error) { r, err := registry.NewRegistry(srv.runtime.config.Root, nil, srv.HTTPRequestFactory(nil)) if err != nil {