فهرست منبع

Merge pull request #497 from justone/dot-graph-images

+ images: output graph of images to dot (graphviz)
Guillaume J. Charmes 12 سال پیش
والد
کامیت
8472a27e80
4فایلهای تغییر یافته به همراه186 افزوده شده و 64 حذف شده
  1. 105 64
      commands.go
  2. 71 0
      commands_test.go
  3. 10 0
      docs/sources/commandline/command/images.rst
  4. BIN
      docs/sources/commandline/command/images/docker_images.gif

+ 105 - 64
commands.go

@@ -678,85 +678,126 @@ func (srv *Server) CmdImages(stdin io.ReadCloser, stdout io.Writer, args ...stri
 	//limit := cmd.Int("l", 0, "Only show the N most recent versions of each image")
 	quiet := cmd.Bool("q", false, "only show numeric IDs")
 	flAll := cmd.Bool("a", false, "show all images")
+	flViz := cmd.Bool("viz", false, "output graph in graphviz format")
 	if err := cmd.Parse(args); err != nil {
 		return nil
 	}
-	if cmd.NArg() > 1 {
-		cmd.Usage()
-		return nil
-	}
-	var nameFilter string
-	if cmd.NArg() == 1 {
-		nameFilter = cmd.Arg(0)
-	}
-	w := tabwriter.NewWriter(stdout, 20, 1, 3, ' ', 0)
-	if !*quiet {
-		fmt.Fprintln(w, "REPOSITORY\tTAG\tID\tCREATED")
-	}
-	var allImages map[string]*Image
-	var err error
-	if *flAll {
-		allImages, err = srv.runtime.graph.Map()
-	} else {
-		allImages, err = srv.runtime.graph.Heads()
-	}
-	if err != nil {
-		return err
-	}
-	for name, repository := range srv.runtime.repositories.Repositories {
-		if nameFilter != "" && name != nameFilter {
-			continue
+
+	if *flViz {
+		images, _ := srv.runtime.graph.All()
+		if images == nil {
+			return nil
 		}
-		for tag, id := range repository {
-			image, err := srv.runtime.graph.Get(id)
+
+		fmt.Fprintf(stdout, "digraph docker {\n")
+
+		var parentImage *Image
+		var err error
+		for _, image := range images {
+			parentImage, err = image.GetParent()
 			if err != nil {
-				log.Printf("Warning: couldn't load %s from %s/%s: %s", id, name, tag, err)
+				fmt.Errorf("Error while getting parent image: %v", err)
+				return nil
+			}
+			if parentImage != nil {
+				fmt.Fprintf(stdout, "  \"%s\" -> \"%s\"\n", parentImage.ShortId(), image.ShortId())
+			} else {
+				fmt.Fprintf(stdout, "  base -> \"%s\" [style=invis]\n", image.ShortId())
+			}
+		}
+
+		reporefs := make(map[string][]string)
+
+		for name, repository := range srv.runtime.repositories.Repositories {
+			for tag, id := range repository {
+				reporefs[TruncateId(id)] = append(reporefs[TruncateId(id)], fmt.Sprintf("%s:%s", name, tag))
+			}
+		}
+
+		for id, repos := range reporefs {
+			fmt.Fprintf(stdout, "  \"%s\" [label=\"%s\\n%s\",shape=box,fillcolor=\"paleturquoise\",style=\"filled,rounded\"];\n", id, id, strings.Join(repos, "\\n"))
+		}
+
+		fmt.Fprintf(stdout, "  base [style=invisible]\n")
+		fmt.Fprintf(stdout, "}\n")
+	} else {
+		if cmd.NArg() > 1 {
+			cmd.Usage()
+			return nil
+		}
+		var nameFilter string
+		if cmd.NArg() == 1 {
+			nameFilter = cmd.Arg(0)
+		}
+		w := tabwriter.NewWriter(stdout, 20, 1, 3, ' ', 0)
+		if !*quiet {
+			fmt.Fprintln(w, "REPOSITORY\tTAG\tID\tCREATED")
+		}
+		var allImages map[string]*Image
+		var err error
+		if *flAll {
+			allImages, err = srv.runtime.graph.Map()
+		} else {
+			allImages, err = srv.runtime.graph.Heads()
+		}
+		if err != nil {
+			return err
+		}
+		for name, repository := range srv.runtime.repositories.Repositories {
+			if nameFilter != "" && name != nameFilter {
 				continue
 			}
-			delete(allImages, id)
-			if !*quiet {
-				for idx, field := range []string{
-					/* REPOSITORY */ name,
-					/* TAG */ tag,
-					/* ID */ TruncateId(id),
-					/* CREATED */ HumanDuration(time.Now().Sub(image.Created)) + " ago",
-				} {
-					if idx == 0 {
-						w.Write([]byte(field))
-					} else {
-						w.Write([]byte("\t" + field))
+			for tag, id := range repository {
+				image, err := srv.runtime.graph.Get(id)
+				if err != nil {
+					log.Printf("Warning: couldn't load %s from %s/%s: %s", id, name, tag, err)
+					continue
+				}
+				delete(allImages, id)
+				if !*quiet {
+					for idx, field := range []string{
+						/* REPOSITORY */ name,
+						/* TAG */ tag,
+						/* ID */ TruncateId(id),
+						/* CREATED */ HumanDuration(time.Now().Sub(image.Created)) + " ago",
+					} {
+						if idx == 0 {
+							w.Write([]byte(field))
+						} else {
+							w.Write([]byte("\t" + field))
+						}
 					}
+					w.Write([]byte{'\n'})
+				} else {
+					stdout.Write([]byte(image.ShortId() + "\n"))
 				}
-				w.Write([]byte{'\n'})
-			} else {
-				stdout.Write([]byte(image.ShortId() + "\n"))
 			}
 		}
-	}
-	// Display images which aren't part of a
-	if nameFilter == "" {
-		for id, image := range allImages {
-			if !*quiet {
-				for idx, field := range []string{
-					/* REPOSITORY */ "<none>",
-					/* TAG */ "<none>",
-					/* ID */ TruncateId(id),
-					/* CREATED */ HumanDuration(time.Now().Sub(image.Created)) + " ago",
-				} {
-					if idx == 0 {
-						w.Write([]byte(field))
-					} else {
-						w.Write([]byte("\t" + field))
+		// Display images which aren't part of a
+		if nameFilter == "" {
+			for id, image := range allImages {
+				if !*quiet {
+					for idx, field := range []string{
+						/* REPOSITORY */ "<none>",
+						/* TAG */ "<none>",
+						/* ID */ TruncateId(id),
+						/* CREATED */ HumanDuration(time.Now().Sub(image.Created)) + " ago",
+					} {
+						if idx == 0 {
+							w.Write([]byte(field))
+						} else {
+							w.Write([]byte("\t" + field))
+						}
 					}
+					w.Write([]byte{'\n'})
+				} else {
+					stdout.Write([]byte(image.ShortId() + "\n"))
 				}
-				w.Write([]byte{'\n'})
-			} else {
-				stdout.Write([]byte(image.ShortId() + "\n"))
 			}
 		}
-	}
-	if !*quiet {
-		w.Flush()
+		if !*quiet {
+			w.Flush()
+		}
 	}
 	return nil
 }

+ 71 - 0
commands_test.go

@@ -73,6 +73,77 @@ func cmdWait(srv *Server, container *Container) error {
 	return closeWrap(stdout, stdoutPipe)
 }
 
+func cmdImages(srv *Server, args ...string) (string, error) {
+	stdout, stdoutPipe := io.Pipe()
+
+	go func() {
+		if err := srv.CmdImages(nil, stdoutPipe, args...); err != nil {
+			return
+		}
+
+		// force the pipe closed, so that the code below gets an EOF
+		stdoutPipe.Close()
+	}()
+
+	output, err := ioutil.ReadAll(stdout)
+	if err != nil {
+		return "", err
+	}
+
+	// Cleanup pipes
+	return string(output), closeWrap(stdout, stdoutPipe)
+}
+
+// TestImages checks that 'docker images' displays information correctly
+func TestImages(t *testing.T) {
+
+	runtime, err := newTestRuntime()
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer nuke(runtime)
+
+	srv := &Server{runtime: runtime}
+
+	output, err := cmdImages(srv)
+
+	if !strings.Contains(output, "REPOSITORY") {
+		t.Fatal("'images' should have a header")
+	}
+	if !strings.Contains(output, "docker-ut") {
+		t.Fatal("'images' should show the docker-ut image")
+	}
+	if !strings.Contains(output, "e9aa60c60128") {
+		t.Fatal("'images' should show the docker-ut image id")
+	}
+
+	output, err = cmdImages(srv, "-q")
+
+	if strings.Contains(output, "REPOSITORY") {
+		t.Fatal("'images -q' should not have a header")
+	}
+	if strings.Contains(output, "docker-ut") {
+		t.Fatal("'images' should not show the docker-ut image name")
+	}
+	if !strings.Contains(output, "e9aa60c60128") {
+		t.Fatal("'images' should show the docker-ut image id")
+	}
+
+	output, err = cmdImages(srv, "-viz")
+
+	if !strings.HasPrefix(output, "digraph docker {") {
+		t.Fatal("'images -v' should start with the dot header")
+	}
+	if !strings.HasSuffix(output, "}\n") {
+		t.Fatal("'images -v' should end with a '}'")
+	}
+	if !strings.Contains(output, "base -> \"e9aa60c60128\" [style=invis]") {
+		t.Fatal("'images -v' should have the docker-ut image id node")
+	}
+
+	// todo: add checks for -a
+}
+
 // TestRunHostname checks that 'docker run -h' correctly sets a custom hostname
 func TestRunHostname(t *testing.T) {
 	runtime, err := newTestRuntime()

+ 10 - 0
docs/sources/commandline/command/images.rst

@@ -10,3 +10,13 @@
 
       -a=false: show all images
       -q=false: only show numeric IDs
+      -viz=false: output in graphviz format
+
+Displaying images visually
+--------------------------
+
+::
+
+    docker images -viz | dot -Tpng -o docker.png
+
+.. image:: images/docker_images.gif

BIN
docs/sources/commandline/command/images/docker_images.gif