浏览代码

Merge pull request #5518 from vbatts/vbatts-save_many

docker save: ability to save multiple images
Victor Vieux 11 年之前
父节点
当前提交
6eaac7d571

+ 15 - 5
api/client/commands.go

@@ -2257,14 +2257,14 @@ func (cli *DockerCli) CmdCp(args ...string) error {
 }
 
 func (cli *DockerCli) CmdSave(args ...string) error {
-	cmd := cli.Subcmd("save", "IMAGE", "Save an image to a tar archive (streamed to STDOUT by default)")
+	cmd := cli.Subcmd("save", "IMAGE [IMAGE...]", "Save an image(s) to a tar archive (streamed to STDOUT by default)")
 	outfile := cmd.String([]string{"o", "-output"}, "", "Write to an file, instead of STDOUT")
 
 	if err := cmd.Parse(args); err != nil {
 		return err
 	}
 
-	if cmd.NArg() != 1 {
+	if cmd.NArg() < 1 {
 		cmd.Usage()
 		return nil
 	}
@@ -2279,9 +2279,19 @@ func (cli *DockerCli) CmdSave(args ...string) error {
 			return err
 		}
 	}
-	image := cmd.Arg(0)
-	if err := cli.stream("GET", "/images/"+image+"/get", nil, output, nil); err != nil {
-		return err
+	if len(cmd.Args()) == 1 {
+		image := cmd.Arg(0)
+		if err := cli.stream("GET", "/images/"+image+"/get", nil, output, nil); err != nil {
+			return err
+		}
+	} else {
+		v := url.Values{}
+		for _, arg := range cmd.Args() {
+			v.Add("names", arg)
+		}
+		if err := cli.stream("GET", "/images/get?"+v.Encode(), nil, output, nil); err != nil {
+			return err
+		}
 	}
 	return nil
 }

+ 10 - 1
api/server/server.go

@@ -611,10 +611,18 @@ func getImagesGet(eng *engine.Engine, version version.Version, w http.ResponseWr
 	if vars == nil {
 		return fmt.Errorf("Missing parameter")
 	}
+	if err := parseForm(r); err != nil {
+		return err
+	}
 	if version.GreaterThan("1.0") {
 		w.Header().Set("Content-Type", "application/x-tar")
 	}
-	job := eng.Job("image_export", vars["name"])
+	var job *engine.Job
+	if name, ok := vars["name"]; ok {
+		job = eng.Job("image_export", name)
+	} else {
+		job = eng.Job("image_export", r.Form["names"]...)
+	}
 	job.Stdout.Add(w)
 	return job.Run()
 }
@@ -1105,6 +1113,7 @@ func createRouter(eng *engine.Engine, logging, enableCors bool, dockerVersion st
 			"/images/json":                    getImagesJSON,
 			"/images/viz":                     getImagesViz,
 			"/images/search":                  getImagesSearch,
+			"/images/get":                     getImagesGet,
 			"/images/{name:.*}/get":           getImagesGet,
 			"/images/{name:.*}/history":       getImagesHistory,
 			"/images/{name:.*}/json":          getImagesByName,

+ 36 - 3
docs/sources/reference/api/docker_remote_api_v1.15.md

@@ -1333,12 +1333,17 @@ via polling (using since)
     -   **200** – no error
     -   **500** – server error
 
-### Get a tarball containing all images and tags in a repository
+### Get a tarball containing all images in a repository
 
 `GET /images/(name)/get`
 
-Get a tarball containing all images and metadata for the repository
-specified by `name`.
+Get a tarball containing all images and metadata for the repository specified
+by `name`.
+
+If `name` is a specific name and tag (e.g. ubuntu:latest), then only that image
+(and its parents) are returned. If `name` is an image ID, similarly only that
+image (and its parents) are returned, but with the exclusion of the
+'repositories' file in the tarball, as there were no image names referenced.
 
     **Example request**
 
@@ -1356,6 +1361,34 @@ specified by `name`.
     -   **200** – no error
     -   **500** – server error
 
+### Get a tarball containing of images.
+
+`GET /images/get`
+
+Get a tarball containing all images and metadata for one or more repositories.
+
+For each value of the `names` parameter: if it is a specific name and tag (e.g.
+ubuntu:latest), then only that image (and its parents) are returned; if it is
+an image ID, similarly only that image (and its parents) are returned and there
+would be no names referenced in the 'repositories' file for this image ID.
+
+
+    **Example request**
+
+        GET /images/get?names=myname%2Fmyapp%3Alatest&names=busybox
+
+    **Example response**:
+
+        HTTP/1.1 200 OK
+        Content-Type: application/x-tar
+
+        Binary data stream
+
+    Status Codes:
+
+    -   **200** – no error
+    -   **500** – server error
+
 ### Load a tarball with a set of images and tags into docker
 
 `POST /images/load`

+ 11 - 6
docs/sources/reference/commandline/cli.md

@@ -1249,17 +1249,17 @@ Providing a maximum restart limit is only valid for the ** on-failure ** policy.
 
 ## save
 
-    Usage: docker save [OPTIONS] IMAGE
+    Usage: docker save [OPTIONS] IMAGE [IMAGE...]
 
-    Save an image to a tar archive (streamed to STDOUT by default)
+    Save an image(s) to a tar archive (streamed to STDOUT by default)
 
       -o, --output=""    Write to an file, instead of STDOUT
 
-Produces a tarred repository to the standard output stream. Contains all
-parent layers, and all tags + versions, or specified repo:tag.
+Produces a tarred repository to the standard output stream.
+Contains all parent layers, and all tags + versions, or specified repo:tag, for
+each argument provided.
 
-It is used to create a backup that can then be used with
-`docker load`
+It is used to create a backup that can then be used with ``docker load``
 
     $ sudo docker save busybox > busybox.tar
     $ ls -sh busybox.tar
@@ -1270,6 +1270,11 @@ It is used to create a backup that can then be used with
     $ sudo docker save -o fedora-all.tar fedora
     $ sudo docker save -o fedora-latest.tar fedora:latest
 
+It is even useful to cherry-pick particular tags of an image repository
+
+   $ sudo docker save -o ubuntu.tar ubuntu:lucid ubuntu:saucy
+
+
 ## search
 
 Search [Docker Hub](https://hub.docker.com) for images

+ 57 - 36
graph/export.go

@@ -19,10 +19,9 @@ import (
 // name is the set of tags to export.
 // out is the writer where the images are written to.
 func (s *TagStore) CmdImageExport(job *engine.Job) engine.Status {
-	if len(job.Args) != 1 {
-		return job.Errorf("Usage: %s IMAGE\n", job.Name)
+	if len(job.Args) < 1 {
+		return job.Errorf("Usage: %s IMAGE [IMAGE...]\n", job.Name)
 	}
-	name := job.Args[0]
 	// get image json
 	tempdir, err := ioutil.TempDir("", "docker-export-")
 	if err != nil {
@@ -30,49 +29,71 @@ func (s *TagStore) CmdImageExport(job *engine.Job) engine.Status {
 	}
 	defer os.RemoveAll(tempdir)
 
-	log.Debugf("Serializing %s", name)
-
 	rootRepoMap := map[string]Repository{}
-	rootRepo, err := s.Get(name)
-	if err != nil {
-		return job.Error(err)
-	}
-	if rootRepo != nil {
-		// this is a base repo name, like 'busybox'
+	for _, name := range job.Args {
+		log.Debugf("Serializing %s", name)
+		rootRepo := s.Repositories[name]
+		if rootRepo != nil {
+			// this is a base repo name, like 'busybox'
+			for _, id := range rootRepo {
+				if _, ok := rootRepoMap[name]; !ok {
+					rootRepoMap[name] = rootRepo
+				} else {
+					log.Debugf("Duplicate key [%s]", name)
+					if rootRepoMap[name].Contains(rootRepo) {
+						log.Debugf("skipping, because it is present [%s:%q]", name, rootRepo)
+						continue
+					}
+					log.Debugf("updating [%s]: [%q] with [%q]", name, rootRepoMap[name], rootRepo)
+					rootRepoMap[name].Update(rootRepo)
+				}
 
-		for _, id := range rootRepo {
-			if err := s.exportImage(job.Eng, id, tempdir); err != nil {
-				return job.Error(err)
-			}
-		}
-		rootRepoMap[name] = rootRepo
-	} else {
-		img, err := s.LookupImage(name)
-		if err != nil {
-			return job.Error(err)
-		}
-		if img != nil {
-			// This is a named image like 'busybox:latest'
-			repoName, repoTag := parsers.ParseRepositoryTag(name)
-			if err := s.exportImage(job.Eng, img.ID, tempdir); err != nil {
-				return job.Error(err)
-			}
-			// check this length, because a lookup of a truncated has will not have a tag
-			// and will not need to be added to this map
-			if len(repoTag) > 0 {
-				rootRepoMap[repoName] = Repository{repoTag: img.ID}
+				if err := s.exportImage(job.Eng, id, tempdir); err != nil {
+					return job.Error(err)
+				}
 			}
 		} else {
-			// this must be an ID that didn't get looked up just right?
-			if err := s.exportImage(job.Eng, name, tempdir); err != nil {
+			img, err := s.LookupImage(name)
+			if err != nil {
 				return job.Error(err)
 			}
+
+			if img != nil {
+				// This is a named image like 'busybox:latest'
+				repoName, repoTag := parsers.ParseRepositoryTag(name)
+
+				// check this length, because a lookup of a truncated has will not have a tag
+				// and will not need to be added to this map
+				if len(repoTag) > 0 {
+					if _, ok := rootRepoMap[repoName]; !ok {
+						rootRepoMap[repoName] = Repository{repoTag: img.ID}
+					} else {
+						log.Debugf("Duplicate key [%s]", repoName)
+						newRepo := Repository{repoTag: img.ID}
+						if rootRepoMap[repoName].Contains(newRepo) {
+							log.Debugf("skipping, because it is present [%s:%q]", repoName, newRepo)
+							continue
+						}
+						log.Debugf("updating [%s]: [%q] with [%q]", repoName, rootRepoMap[repoName], newRepo)
+						rootRepoMap[repoName].Update(newRepo)
+					}
+				}
+				if err := s.exportImage(job.Eng, img.ID, tempdir); err != nil {
+					return job.Error(err)
+				}
+
+			} else {
+				// this must be an ID that didn't get looked up just right?
+				if err := s.exportImage(job.Eng, name, tempdir); err != nil {
+					return job.Error(err)
+				}
+			}
 		}
+		log.Debugf("End Serializing %s", name)
 	}
 	// write repositories, if there is something to write
 	if len(rootRepoMap) > 0 {
 		rootRepoJson, _ := json.Marshal(rootRepoMap)
-
 		if err := ioutil.WriteFile(path.Join(tempdir, "repositories"), rootRepoJson, os.FileMode(0644)); err != nil {
 			return job.Error(err)
 		}
@@ -89,7 +110,7 @@ func (s *TagStore) CmdImageExport(job *engine.Job) engine.Status {
 	if _, err := io.Copy(job.Stdout, fs); err != nil {
 		return job.Error(err)
 	}
-	log.Debugf("End Serializing %s", name)
+	log.Debugf("End export job: %s", job.Name)
 	return engine.StatusOK
 }
 

+ 18 - 0
graph/tags.go

@@ -30,6 +30,24 @@ type TagStore struct {
 
 type Repository map[string]string
 
+// update Repository mapping with content of u
+func (r Repository) Update(u Repository) {
+	for k, v := range u {
+		r[k] = v
+	}
+}
+
+// return true if the contents of u Repository, are wholly contained in r Repository
+func (r Repository) Contains(u Repository) bool {
+	for k, v := range u {
+		// if u's key is not present in r OR u's key is present, but not the same value
+		if rv, ok := r[k]; !ok || (ok && rv != v) {
+			return false
+		}
+	}
+	return true
+}
+
 func NewTagStore(path string, graph *Graph) (*TagStore, error) {
 	abspath, err := filepath.Abs(path)
 	if err != nil {

+ 24 - 0
integration-cli/docker_cli_save_load_test.go

@@ -167,3 +167,27 @@ func TestSaveAndLoadRepoFlags(t *testing.T) {
 	logDone("save - save a repo using -o")
 	logDone("load - load a repo using -i")
 }
+
+func TestSaveMultipleNames(t *testing.T) {
+	repoName := "foobar-save-multi-name-test"
+
+	// Make one image
+	tagCmdFinal := fmt.Sprintf("%v tag scratch:latest %v-one:latest", dockerBinary, repoName)
+	tagCmd := exec.Command("bash", "-c", tagCmdFinal)
+	out, _, err := runCommandWithOutput(tagCmd)
+	errorOut(err, t, fmt.Sprintf("failed to tag repo: %v %v", out, err))
+	// Make two images
+	tagCmdFinal = fmt.Sprintf("%v tag scratch:latest %v-two:latest", dockerBinary, repoName)
+	tagCmd = exec.Command("bash", "-c", tagCmdFinal)
+	out, _, err = runCommandWithOutput(tagCmd)
+	errorOut(err, t, fmt.Sprintf("failed to tag repo: %v %v", out, err))
+
+	saveCmdFinal := fmt.Sprintf("%v save %v-one %v-two:latest | tar xO repositories | grep -q -E '(-one|-two)'", dockerBinary, repoName, repoName)
+	saveCmd := exec.Command("bash", "-c", saveCmdFinal)
+	out, _, err = runCommandWithOutput(saveCmd)
+	errorOut(err, t, fmt.Sprintf("failed to save multiple repos: %v %v", out, err))
+
+	deleteImages(repoName)
+
+	logDone("save - save by multiple names")
+}