diff --git a/README.md b/README.md index 55cbfb4..d6e30e4 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,20 @@ Image info is visualized with lines indicating parent images: ``` $ dockviz images -d | dot -Tpng -o images.png +OR +$ dockviz images --dot | dot -Tpng -o images.png ``` ![](sample/images.png "Image") +``` +$ dockviz images -d -l | dot -Tpng -o images.png +OR +$ dockviz images --dot -only-labeled | dot -Tpng -o images.png +``` + +![](sample/images_only_labeled.png "Image") + Or in short form: ``` @@ -52,35 +62,47 @@ Or as a tree in the terminal: ``` $ dockviz images -t └─511136ea3c5a Virtual Size: 0.0 B - |─f10ebce2c0e1 Virtual Size: 103.7 MB - | └─82cdea7ab5b5 Virtual Size: 103.9 MB - | └─5dbd9cb5a02f Virtual Size: 103.9 MB - | └─74fe38d11401 Virtual Size: 209.6 MB Tags: ubuntu:12.04, ubuntu:precise - |─ef519c9ee91a Virtual Size: 100.9 MB - | └─07302703becc Virtual Size: 101.2 MB - | └─cf8dc907452c Virtual Size: 101.2 MB - | └─a7cf8ae4e998 Virtual Size: 171.3 MB Tags: ubuntu:12.10, ubuntu:quantal - | |─e18d8001204e Virtual Size: 171.3 MB - | | └─d0525208a46c Virtual Size: 171.3 MB - | | └─59dac4bae93b Virtual Size: 242.5 MB - | | └─89541b3b35f2 Virtual Size: 511.8 MB - | | └─7dac4e98548e Virtual Size: 511.8 MB - | | └─341d0cc3fac8 Virtual Size: 511.8 MB - | | └─2f96171d2098 Virtual Size: 511.8 MB - | | └─67b8b7262a67 Virtual Size: 513.7 MB - | | └─0fe9a2bc50fe Virtual Size: 513.7 MB - | | └─8c32832f07ba Virtual Size: 513.7 MB - | | └─cc4e1358bc80 Virtual Size: 513.7 MB - | | └─5c0d04fba9df Virtual Size: 513.7 MB Tags: nate/mongodb:latest - | └─398d592f2009 Virtual Size: 242.2 MB - | └─0cd8e7f50270 Virtual Size: 243.6 MB - | └─594b6f8e6f92 Virtual Size: 243.6 MB - | └─f832a63e87a4 Virtual Size: 243.6 MB Tags: redis:latest + ├─f10ebce2c0e1 Virtual Size: 103.7 MB + │ └─82cdea7ab5b5 Virtual Size: 103.9 MB + │ └─5dbd9cb5a02f Virtual Size: 103.9 MB + │ └─74fe38d11401 Virtual Size: 209.6 MB Tags: ubuntu:12.04, ubuntu:precise + ├─ef519c9ee91a Virtual Size: 100.9 MB + │ └─07302703becc Virtual Size: 101.2 MB + │ └─cf8dc907452c Virtual Size: 101.2 MB + │ └─a7cf8ae4e998 Virtual Size: 171.3 MB Tags: ubuntu:12.10, ubuntu:quantal + │ │─e18d8001204e Virtual Size: 171.3 MB + │ │ └─d0525208a46c Virtual Size: 171.3 MB + │ │ └─59dac4bae93b Virtual Size: 242.5 MB + │ │ └─89541b3b35f2 Virtual Size: 511.8 MB + │ │ └─7dac4e98548e Virtual Size: 511.8 MB + │ │ └─341d0cc3fac8 Virtual Size: 511.8 MB + │ │ └─2f96171d2098 Virtual Size: 511.8 MB + │ │ └─67b8b7262a67 Virtual Size: 513.7 MB + │ │ └─0fe9a2bc50fe Virtual Size: 513.7 MB + │ │ └─8c32832f07ba Virtual Size: 513.7 MB + │ │ └─cc4e1358bc80 Virtual Size: 513.7 MB + │ │ └─5c0d04fba9df Virtual Size: 513.7 MB Tags: nate/mongodb:latest + │ └─398d592f2009 Virtual Size: 242.2 MB + │ └─0cd8e7f50270 Virtual Size: 243.6 MB + │ └─594b6f8e6f92 Virtual Size: 243.6 MB + │ └─f832a63e87a4 Virtual Size: 243.6 MB Tags: redis:latest └─02dae1c13f51 Virtual Size: 98.3 MB └─e7206bfc66aa Virtual Size: 98.5 MB └─cb12405ee8fa Virtual Size: 98.5 MB └─316b678ddf48 Virtual Size: 169.4 MB Tags: ubuntu:13.04, ubuntu:raring ``` +``` +$ dockviz images -t -l +└─511136ea3c5a Virtual Size: 0.0 B + ├─f10ebce2c0e1 Virtual Size: 103.7 MB + │ └─74fe38d11401 Virtual Size: 209.6 MB Tags: ubuntu:12.04, ubuntu:precise + ├─ef519c9ee91a Virtual Size: 100.9 MB + │ └─a7cf8ae4e998 Virtual Size: 171.3 MB Tags: ubuntu:12.10, ubuntu:quantal + │ ├─5c0d04fba9df Virtual Size: 513.7 MB Tags: nate/mongodb:latest + │ └─f832a63e87a4 Virtual Size: 243.6 MB Tags: redis:latest + └─02dae1c13f51 Virtual Size: 98.3 MB + └─316b678ddf48 Virtual Size: 169.4 MB Tags: ubuntu:13.04, ubuntu:raring +``` # Running diff --git a/images.go b/images.go index 0fa9409..c2893e6 100644 --- a/images.go +++ b/images.go @@ -21,16 +21,16 @@ type Image struct { } type ImagesCommand struct { - Dot bool `short:"d" long:"dot" description:"Show image information as Graphviz dot."` - Tree bool `short:"t" long:"tree" description:"Show image information as tree."` - Short bool `short:"s" long:"short" description:"Show short summary of images (repo name and list of tags)."` - NoTruncate bool `short:"n" long:"no-trunc" description:"Don't truncate the image IDs."` + Dot bool `short:"d" long:"dot" description:"Show image information as Graphviz dot. You can add a start image id or name -d/--dot [id/name]"` + Tree bool `short:"t" long:"tree" description:"Show image information as tree. You can add a start image id or name -t/--tree [id/name]"` + Short bool `short:"s" long:"short" description:"Show short summary of images (repo name and list of tags)."` + NoTruncate bool `short:"n" long:"no-trunc" description:"Don't truncate the image IDs."` + OnlyLabelled bool `short:"l" long:"only-labelled" description:"Print only labelled images/containers."` } var imagesCommand ImagesCommand func (x *ImagesCommand) Execute(args []string) error { - var images *[]Image stat, err := os.Stdin.Stat() @@ -82,50 +82,40 @@ func (x *ImagesCommand) Execute(args []string) error { images = &ims } - if imagesCommand.Dot { - fmt.Printf(jsonToDot(images)) - } else if imagesCommand.Tree { - - var startImage = "" + if imagesCommand.Tree || imagesCommand.Dot { + var startImage *Image if len(args) > 0 { + startImage, err = findStartImage(args[0], images) - // attempt to find the start image, which can be specified as an - // image ID or a repository name - - startImageArg := args[0] - startImageRepo := args[0] - - // in case a repo name was specified, append ":latest" if it isn't - // already there - if !strings.HasSuffix(startImageRepo, ":latest") { - startImageRepo = fmt.Sprintf("%s:latest", startImageRepo) - } - - IMAGES: - for _, image := range *images { - // check if the start image arg matches an image id - if strings.Index(image.Id, startImageArg) == 0 { - startImage = image.Id - break IMAGES - } - - // check if the start image arg matches an repository name - if image.RepoTags[0] != ":" { - for _, repotag := range image.RepoTags { - if repotag == startImageRepo { - startImage = image.Id - break IMAGES - } - } - } - } - - if startImage == "" { - return fmt.Errorf("Unable to find image %s.", startImageArg) + if err != nil { + return err } } - fmt.Printf(jsonToTree(images, startImage, imagesCommand.NoTruncate)) + // select the start image of the tree + var roots []Image + if startImage == nil { + roots = collectRoots(images) + } else { + startImage.ParentId = "" + roots = []Image{*startImage} + } + + // build helper map (image -> children) + imagesByParent := collectChildren(images) + + // filter images + if imagesCommand.OnlyLabelled { + *images, imagesByParent = filterImages(images, &imagesByParent) + } + + if imagesCommand.Tree { + fmt.Print(jsonToTree(imagesCommand.NoTruncate, roots, imagesByParent)) + } + if imagesCommand.Dot { + fmt.Print(jsonToDot(roots, imagesByParent)) + } + } else if imagesCommand.Short { fmt.Printf(jsonToShort(images)) } else { @@ -135,67 +125,137 @@ func (x *ImagesCommand) Execute(args []string) error { return nil } -func jsonToTree(images *[]Image, startImageArg string, noTrunc bool) string { +func findStartImage(name string, images *[]Image) (*Image, error) { + + var startImage *Image + + // attempt to find the start image, which can be specified as an + // image ID or a repository name + startImageArg := name + startImageRepo := name + + // if tag is not defined, find by :latest tag + if strings.Index(startImageRepo, ":") == -1 { + startImageRepo = fmt.Sprintf("%s:latest", startImageRepo) + } + +IMAGES: + for _, image := range *images { + // find by image id + if strings.Index(image.Id, startImageArg) == 0 { + startImage = &image + break IMAGES + } + + // find by image name (name and tag) + for _, repotag := range image.RepoTags { + if repotag == startImageRepo { + startImage = &image + break IMAGES + } + } + } + + if startImage == nil { + return nil, fmt.Errorf("Unable to find image %s = %s.", startImageArg, startImageRepo) + } + + return startImage, nil +} + +func jsonToTree(noTrunc bool, images []Image, byParent map[string][]Image) string { var buffer bytes.Buffer - var startImage Image - - var roots []Image - var byParent = make(map[string][]Image) - for _, image := range *images { - if image.ParentId == "" { - roots = append(roots, image) - } else { - if children, exists := byParent[image.ParentId]; exists { - byParent[image.ParentId] = append(children, image) - } else { - byParent[image.ParentId] = []Image{image} - } - } - - if startImageArg != "" { - if startImageArg == image.Id || startImageArg == truncate(image.Id) { - startImage = image - } - - for _, repotag := range image.RepoTags { - if repotag == startImageArg { - startImage = image - } - } - } - } - - if startImageArg != "" { - WalkTree(&buffer, noTrunc, []Image{startImage}, byParent, "") - } else { - WalkTree(&buffer, noTrunc, roots, byParent, "") - } + jsonToText(&buffer, noTrunc, images, byParent, "") return buffer.String() } -func WalkTree(buffer *bytes.Buffer, noTrunc bool, images []Image, byParent map[string][]Image, prefix string) { - if len(images) > 1 { - length := len(images) +func jsonToDot(roots []Image, byParent map[string][]Image) string { + var buffer bytes.Buffer + + buffer.WriteString("digraph docker {\n") + imagesToDot(&buffer, roots, byParent) + buffer.WriteString(" base [style=invisible]\n}\n") + + return buffer.String() +} + +func collectChildren(images *[]Image) map[string][]Image { + var imagesByParent = make(map[string][]Image) + for _, image := range *images { + if children, exists := imagesByParent[image.ParentId]; exists { + imagesByParent[image.ParentId] = append(children, image) + } else { + imagesByParent[image.ParentId] = []Image{image} + } + } + + return imagesByParent +} + +func collectRoots(images *[]Image) []Image { + var roots []Image + for _, image := range *images { + if image.ParentId == "" { + roots = append(roots, image) + } + } + + return roots +} + +func filterImages(images *[]Image, byParent *map[string][]Image) (filteredImages []Image, filteredChildren map[string][]Image) { + for i := 0; i < len(*images); i++ { + // image is visible + // 1. it has a label + // 2. it is root + // 3. it is a node + var visible bool = (*images)[i].RepoTags[0] != ":" || (*images)[i].ParentId == "" || len((*byParent)[(*images)[i].Id]) > 1 + if visible { + filteredImages = append(filteredImages, (*images)[i]) + } else { + // change childs parent id + // if items are filtered with only one child + for j := 0; j < len(filteredImages); j++ { + if filteredImages[j].ParentId == (*images)[i].Id { + filteredImages[j].ParentId = (*images)[i].ParentId + } + } + for j := 0; j < len(*images); j++ { + if (*images)[j].ParentId == (*images)[i].Id { + (*images)[j].ParentId = (*images)[i].ParentId + } + } + } + } + + filteredChildren = collectChildren(&filteredImages) + + return filteredImages, filteredChildren +} + +func jsonToText(buffer *bytes.Buffer, noTrunc bool, images []Image, byParent map[string][]Image, prefix string) { + var length = len(images) + if length > 1 { for index, image := range images { + var nextPrefix string = "" if index+1 == length { PrintTreeNode(buffer, noTrunc, image, prefix+"└─") - if subimages, exists := byParent[image.Id]; exists { - WalkTree(buffer, noTrunc, subimages, byParent, prefix+" ") - } + nextPrefix = " " } else { PrintTreeNode(buffer, noTrunc, image, prefix+"├─") - if subimages, exists := byParent[image.Id]; exists { - WalkTree(buffer, noTrunc, subimages, byParent, prefix+"│ ") - } + nextPrefix = "│ " + } + if subimages, exists := byParent[image.Id]; exists { + jsonToText(buffer, noTrunc, subimages, byParent, prefix+nextPrefix) } } } else { for _, image := range images { PrintTreeNode(buffer, noTrunc, image, prefix+"└─") if subimages, exists := byParent[image.Id]; exists { - WalkTree(buffer, noTrunc, subimages, byParent, prefix+" ") + jsonToText(buffer, noTrunc, subimages, byParent, prefix+" ") } } } @@ -251,12 +311,8 @@ func parseImagesJSON(rawJSON []byte) (*[]Image, error) { return &images, nil } -func jsonToDot(images *[]Image) string { - - var buffer bytes.Buffer - buffer.WriteString("digraph docker {\n") - - for _, image := range *images { +func imagesToDot(buffer *bytes.Buffer, images []Image, byParent map[string][]Image) { + for _, image := range images { if image.ParentId == "" { buffer.WriteString(fmt.Sprintf(" base -> \"%s\" [style=invis]\n", truncate(image.Id))) } else { @@ -265,11 +321,10 @@ func jsonToDot(images *[]Image) string { if image.RepoTags[0] != ":" { buffer.WriteString(fmt.Sprintf(" \"%s\" [label=\"%s\\n%s\",shape=box,fillcolor=\"paleturquoise\",style=\"filled,rounded\"];\n", truncate(image.Id), truncate(image.Id), strings.Join(image.RepoTags, "\\n"))) } + if subimages, exists := byParent[image.Id]; exists { + imagesToDot(buffer, subimages, byParent) + } } - - buffer.WriteString(" base [style=invisible]\n}\n") - - return buffer.String() } func jsonToShort(images *[]Image) string { diff --git a/images_only_labeled.png b/images_only_labeled.png new file mode 100644 index 0000000..089c6e2 Binary files /dev/null and b/images_only_labeled.png differ diff --git a/images_test.go b/images_test.go index 09b32c3..b88f154 100644 --- a/images_test.go +++ b/images_test.go @@ -56,7 +56,12 @@ func Test_Dot(t *testing.T) { for _, dotTest := range dotTests { im, _ := parseImagesJSON([]byte(dotTest.json)) - result := jsonToDot(im) + byParent := collectChildren(im) + roots := collectRoots(im) + + // TODO: test start image limiting + + result := jsonToDot(roots, byParent) for _, regexp := range allRegex { if !regexp.MatchString(result) { @@ -109,7 +114,16 @@ func Test_Tree(t *testing.T) { for _, treeTest := range treeTests { im, _ := parseImagesJSON([]byte(treeTest.json)) - result := jsonToTree(im, treeTest.startImage, treeTest.noTrunc) + byParent := collectChildren(im) + var roots []Image + if len(treeTest.startImage) > 0 { + startImage, _ := findStartImage(treeTest.startImage, im) + startImage.ParentId = "" + roots = []Image{*startImage} + } else { + roots = collectRoots(im) + } + result := jsonToTree(treeTest.noTrunc, roots, byParent) for _, regexp := range compileRegexps(t, treeTest.regexps) { if !regexp.MatchString(result) { diff --git a/sample/images.png b/sample/images.png index 4b6d3cf..9dd4a2d 100644 Binary files a/sample/images.png and b/sample/images.png differ diff --git a/sample/images_only_labeled.png b/sample/images_only_labeled.png new file mode 100644 index 0000000..089c6e2 Binary files /dev/null and b/sample/images_only_labeled.png differ