+ images: output graph of images to dot (graphviz)
Guillaume J. Charmes authored on 2013/05/07 09:48:07... | ... |
@@ -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 |