Browse code

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

+ images: output graph of images to dot (graphviz)

Guillaume J. Charmes authored on 2013/05/07 09:48:07
Showing 4 changed files
... ...
@@ -678,85 +678,126 @@ func (srv *Server) CmdImages(stdin io.ReadCloser, stdout io.Writer, args ...stri
678 678
 	//limit := cmd.Int("l", 0, "Only show the N most recent versions of each image")
679 679
 	quiet := cmd.Bool("q", false, "only show numeric IDs")
680 680
 	flAll := cmd.Bool("a", false, "show all images")
681
+	flViz := cmd.Bool("viz", false, "output graph in graphviz format")
681 682
 	if err := cmd.Parse(args); err != nil {
682 683
 		return nil
683 684
 	}
684
-	if cmd.NArg() > 1 {
685
-		cmd.Usage()
686
-		return nil
687
-	}
688
-	var nameFilter string
689
-	if cmd.NArg() == 1 {
690
-		nameFilter = cmd.Arg(0)
691
-	}
692
-	w := tabwriter.NewWriter(stdout, 20, 1, 3, ' ', 0)
693
-	if !*quiet {
694
-		fmt.Fprintln(w, "REPOSITORY\tTAG\tID\tCREATED")
695
-	}
696
-	var allImages map[string]*Image
697
-	var err error
698
-	if *flAll {
699
-		allImages, err = srv.runtime.graph.Map()
700
-	} else {
701
-		allImages, err = srv.runtime.graph.Heads()
702
-	}
703
-	if err != nil {
704
-		return err
705
-	}
706
-	for name, repository := range srv.runtime.repositories.Repositories {
707
-		if nameFilter != "" && name != nameFilter {
708
-			continue
685
+
686
+	if *flViz {
687
+		images, _ := srv.runtime.graph.All()
688
+		if images == nil {
689
+			return nil
709 690
 		}
710
-		for tag, id := range repository {
711
-			image, err := srv.runtime.graph.Get(id)
691
+
692
+		fmt.Fprintf(stdout, "digraph docker {\n")
693
+
694
+		var parentImage *Image
695
+		var err error
696
+		for _, image := range images {
697
+			parentImage, err = image.GetParent()
712 698
 			if err != nil {
713
-				log.Printf("Warning: couldn't load %s from %s/%s: %s", id, name, tag, err)
699
+				fmt.Errorf("Error while getting parent image: %v", err)
700
+				return nil
701
+			}
702
+			if parentImage != nil {
703
+				fmt.Fprintf(stdout, "  \"%s\" -> \"%s\"\n", parentImage.ShortId(), image.ShortId())
704
+			} else {
705
+				fmt.Fprintf(stdout, "  base -> \"%s\" [style=invis]\n", image.ShortId())
706
+			}
707
+		}
708
+
709
+		reporefs := make(map[string][]string)
710
+
711
+		for name, repository := range srv.runtime.repositories.Repositories {
712
+			for tag, id := range repository {
713
+				reporefs[TruncateId(id)] = append(reporefs[TruncateId(id)], fmt.Sprintf("%s:%s", name, tag))
714
+			}
715
+		}
716
+
717
+		for id, repos := range reporefs {
718
+			fmt.Fprintf(stdout, "  \"%s\" [label=\"%s\\n%s\",shape=box,fillcolor=\"paleturquoise\",style=\"filled,rounded\"];\n", id, id, strings.Join(repos, "\\n"))
719
+		}
720
+
721
+		fmt.Fprintf(stdout, "  base [style=invisible]\n")
722
+		fmt.Fprintf(stdout, "}\n")
723
+	} else {
724
+		if cmd.NArg() > 1 {
725
+			cmd.Usage()
726
+			return nil
727
+		}
728
+		var nameFilter string
729
+		if cmd.NArg() == 1 {
730
+			nameFilter = cmd.Arg(0)
731
+		}
732
+		w := tabwriter.NewWriter(stdout, 20, 1, 3, ' ', 0)
733
+		if !*quiet {
734
+			fmt.Fprintln(w, "REPOSITORY\tTAG\tID\tCREATED")
735
+		}
736
+		var allImages map[string]*Image
737
+		var err error
738
+		if *flAll {
739
+			allImages, err = srv.runtime.graph.Map()
740
+		} else {
741
+			allImages, err = srv.runtime.graph.Heads()
742
+		}
743
+		if err != nil {
744
+			return err
745
+		}
746
+		for name, repository := range srv.runtime.repositories.Repositories {
747
+			if nameFilter != "" && name != nameFilter {
714 748
 				continue
715 749
 			}
716
-			delete(allImages, id)
717
-			if !*quiet {
718
-				for idx, field := range []string{
719
-					/* REPOSITORY */ name,
720
-					/* TAG */ tag,
721
-					/* ID */ TruncateId(id),
722
-					/* CREATED */ HumanDuration(time.Now().Sub(image.Created)) + " ago",
723
-				} {
724
-					if idx == 0 {
725
-						w.Write([]byte(field))
726
-					} else {
727
-						w.Write([]byte("\t" + field))
750
+			for tag, id := range repository {
751
+				image, err := srv.runtime.graph.Get(id)
752
+				if err != nil {
753
+					log.Printf("Warning: couldn't load %s from %s/%s: %s", id, name, tag, err)
754
+					continue
755
+				}
756
+				delete(allImages, id)
757
+				if !*quiet {
758
+					for idx, field := range []string{
759
+						/* REPOSITORY */ name,
760
+						/* TAG */ tag,
761
+						/* ID */ TruncateId(id),
762
+						/* CREATED */ HumanDuration(time.Now().Sub(image.Created)) + " ago",
763
+					} {
764
+						if idx == 0 {
765
+							w.Write([]byte(field))
766
+						} else {
767
+							w.Write([]byte("\t" + field))
768
+						}
728 769
 					}
770
+					w.Write([]byte{'\n'})
771
+				} else {
772
+					stdout.Write([]byte(image.ShortId() + "\n"))
729 773
 				}
730
-				w.Write([]byte{'\n'})
731
-			} else {
732
-				stdout.Write([]byte(image.ShortId() + "\n"))
733 774
 			}
734 775
 		}
735
-	}
736
-	// Display images which aren't part of a
737
-	if nameFilter == "" {
738
-		for id, image := range allImages {
739
-			if !*quiet {
740
-				for idx, field := range []string{
741
-					/* REPOSITORY */ "<none>",
742
-					/* TAG */ "<none>",
743
-					/* ID */ TruncateId(id),
744
-					/* CREATED */ HumanDuration(time.Now().Sub(image.Created)) + " ago",
745
-				} {
746
-					if idx == 0 {
747
-						w.Write([]byte(field))
748
-					} else {
749
-						w.Write([]byte("\t" + field))
776
+		// Display images which aren't part of a
777
+		if nameFilter == "" {
778
+			for id, image := range allImages {
779
+				if !*quiet {
780
+					for idx, field := range []string{
781
+						/* REPOSITORY */ "<none>",
782
+						/* TAG */ "<none>",
783
+						/* ID */ TruncateId(id),
784
+						/* CREATED */ HumanDuration(time.Now().Sub(image.Created)) + " ago",
785
+					} {
786
+						if idx == 0 {
787
+							w.Write([]byte(field))
788
+						} else {
789
+							w.Write([]byte("\t" + field))
790
+						}
750 791
 					}
792
+					w.Write([]byte{'\n'})
793
+				} else {
794
+					stdout.Write([]byte(image.ShortId() + "\n"))
751 795
 				}
752
-				w.Write([]byte{'\n'})
753
-			} else {
754
-				stdout.Write([]byte(image.ShortId() + "\n"))
755 796
 			}
756 797
 		}
757
-	}
758
-	if !*quiet {
759
-		w.Flush()
798
+		if !*quiet {
799
+			w.Flush()
800
+		}
760 801
 	}
761 802
 	return nil
762 803
 }
... ...
@@ -73,6 +73,77 @@ func cmdWait(srv *Server, container *Container) error {
73 73
 	return closeWrap(stdout, stdoutPipe)
74 74
 }
75 75
 
76
+func cmdImages(srv *Server, args ...string) (string, error) {
77
+	stdout, stdoutPipe := io.Pipe()
78
+
79
+	go func() {
80
+		if err := srv.CmdImages(nil, stdoutPipe, args...); err != nil {
81
+			return
82
+		}
83
+
84
+		// force the pipe closed, so that the code below gets an EOF
85
+		stdoutPipe.Close()
86
+	}()
87
+
88
+	output, err := ioutil.ReadAll(stdout)
89
+	if err != nil {
90
+		return "", err
91
+	}
92
+
93
+	// Cleanup pipes
94
+	return string(output), closeWrap(stdout, stdoutPipe)
95
+}
96
+
97
+// TestImages checks that 'docker images' displays information correctly
98
+func TestImages(t *testing.T) {
99
+
100
+	runtime, err := newTestRuntime()
101
+	if err != nil {
102
+		t.Fatal(err)
103
+	}
104
+	defer nuke(runtime)
105
+
106
+	srv := &Server{runtime: runtime}
107
+
108
+	output, err := cmdImages(srv)
109
+
110
+	if !strings.Contains(output, "REPOSITORY") {
111
+		t.Fatal("'images' should have a header")
112
+	}
113
+	if !strings.Contains(output, "docker-ut") {
114
+		t.Fatal("'images' should show the docker-ut image")
115
+	}
116
+	if !strings.Contains(output, "e9aa60c60128") {
117
+		t.Fatal("'images' should show the docker-ut image id")
118
+	}
119
+
120
+	output, err = cmdImages(srv, "-q")
121
+
122
+	if strings.Contains(output, "REPOSITORY") {
123
+		t.Fatal("'images -q' should not have a header")
124
+	}
125
+	if strings.Contains(output, "docker-ut") {
126
+		t.Fatal("'images' should not show the docker-ut image name")
127
+	}
128
+	if !strings.Contains(output, "e9aa60c60128") {
129
+		t.Fatal("'images' should show the docker-ut image id")
130
+	}
131
+
132
+	output, err = cmdImages(srv, "-viz")
133
+
134
+	if !strings.HasPrefix(output, "digraph docker {") {
135
+		t.Fatal("'images -v' should start with the dot header")
136
+	}
137
+	if !strings.HasSuffix(output, "}\n") {
138
+		t.Fatal("'images -v' should end with a '}'")
139
+	}
140
+	if !strings.Contains(output, "base -> \"e9aa60c60128\" [style=invis]") {
141
+		t.Fatal("'images -v' should have the docker-ut image id node")
142
+	}
143
+
144
+	// todo: add checks for -a
145
+}
146
+
76 147
 // TestRunHostname checks that 'docker run -h' correctly sets a custom hostname
77 148
 func TestRunHostname(t *testing.T) {
78 149
 	runtime, err := newTestRuntime()
... ...
@@ -10,3 +10,13 @@
10 10
 
11 11
       -a=false: show all images
12 12
       -q=false: only show numeric IDs
13
+      -viz=false: output in graphviz format
14
+
15
+Displaying images visually
16
+--------------------------
17
+
18
+::
19
+
20
+    docker images -viz | dot -Tpng -o docker.png
21
+
22
+.. image:: images/docker_images.gif
13 23
new file mode 100644
14 24
Binary files /dev/null and b/docs/sources/commandline/command/images/docker_images.gif differ