From 2e787004ab189b8d89ed0c31cb89531f889353e3 Mon Sep 17 00:00:00 2001 From: Nate Jones Date: Sun, 4 May 2014 14:22:25 -0700 Subject: [PATCH] implement --tree --- images.go | 113 +++++++++++++++++++++++++++++++++++++++++++++++-- images_test.go | 41 ++++++++++++++++-- 2 files changed, 147 insertions(+), 7 deletions(-) diff --git a/images.go b/images.go index abc5831..7b79aa9 100644 --- a/images.go +++ b/images.go @@ -19,8 +19,9 @@ 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."` + 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."` + NoTruncate bool `short:"n" long:"notrunc" description:"Don't truncate the image IDs."` } var imagesCommand ImagesCommand @@ -38,12 +39,118 @@ func (x *ImagesCommand) Execute(args []string) error { if imagesCommand.Dot { fmt.Printf(jsonToDot(images)) } else if imagesCommand.Tree { - fmt.Println("Tree output not implemented yet.") + + var startImageArg = "" + if len(args) > 0 { + startImageArg = args[0] + } + + fmt.Printf(jsonToTree(images, startImageArg, imagesCommand.NoTruncate)) } return nil } +func jsonToTree(images *[]Image, startImageArg string, noTrunc bool) 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, "") + } + + 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) + for index, image := range images { + if index+1 == length { + PrintTreeNode(buffer, noTrunc, image, prefix+"└─") + if subimages, exists := byParent[image.Id]; exists { + WalkTree(buffer, noTrunc, subimages, byParent, prefix+" ") + } + } else { + PrintTreeNode(buffer, noTrunc, image, prefix+"|─") + if subimages, exists := byParent[image.Id]; exists { + WalkTree(buffer, noTrunc, subimages, byParent, prefix+"| ") + } + } + } + } else { + for _, image := range images { + PrintTreeNode(buffer, noTrunc, image, prefix+"└─") + if subimages, exists := byParent[image.Id]; exists { + WalkTree(buffer, noTrunc, subimages, byParent, prefix+" ") + } + } + } +} + +func PrintTreeNode(buffer *bytes.Buffer, noTrunc bool, image Image, prefix string) { + var imageID string + if noTrunc { + imageID = image.Id + } else { + imageID = truncate(image.Id) + } + + buffer.WriteString(fmt.Sprintf("%s%s Virtual Size: %s", prefix, imageID, humanSize(image.VirtualSize))) + if image.RepoTags[0] != ":" { + buffer.WriteString(fmt.Sprintf(" Tags: %s\n", strings.Join(image.RepoTags, ", "))) + } else { + buffer.WriteString(fmt.Sprintf("\n")) + } +} + +func humanSize(raw int64) string { + sizes := []string{"B", "KB", "MB", "GB", "TB"} + + rawFloat := float64(raw) + ind := 0 + + for { + if rawFloat < 1000 { + break + } else { + rawFloat = rawFloat / 1000 + ind = ind + 1 + } + } + + return fmt.Sprintf("%.01f %s", rawFloat, sizes[ind]) +} + func truncate(id string) string { return id[0:12] } diff --git a/images_test.go b/images_test.go index f172faf..e80f451 100644 --- a/images_test.go +++ b/images_test.go @@ -5,11 +5,18 @@ import ( "testing" ) -type RunTest struct { +type DotTest struct { json string regexps []string } +type TreeTest struct { + json string + startImage string + noTrunc bool + regexps []string +} + func Test_BadJSON(t *testing.T) { _, err := parseJSON([]byte(` "VirtualSize": 662553464, "Size": 662553464, "RepoTags": [ ":" ], "ParentId": "", "Id": "4c1208b690c68af3476b437e7bc2bcc460f062bda2094d2d8f21a7e70368d358", "Created": 1386114144 }]`)) @@ -25,14 +32,14 @@ func Test_Dot(t *testing.T) { } allRegex := compileRegexps(t, allMatch) - dotTests := []RunTest{ - RunTest{ + dotTests := []DotTest{ + DotTest{ json: `[{ "VirtualSize": 662553464, "Size": 662553464, "RepoTags": [ ":" ], "ParentId": "", "Id": "4c1208b690c68af3476b437e7bc2bcc460f062bda2094d2d8f21a7e70368d358", "Created": 1386114144 }]`, regexps: []string{ `base -> "4c1208b690c6"`, }, }, - RunTest{ + DotTest{ json: `[{ "VirtualSize": 662553464, "Size": 0, "RepoTags": [ "foo:latest" ], "ParentId": "735f5db5626147582d2ae3f2c87be8e5e697c088574c5faaf8d4d1bccab99470", "Id": "c87be8e5e697c735f5db5626147582d2ae3f2088574c5faaf8d4d1bccab99470", "Created": 1386142123 },{ "VirtualSize": 662553464, "Size": 0, "RepoTags": [ ":" ], "ParentId": "4c1208b690c68af3476b437e7bc2bcc460f062bda2094d2d8f21a7e70368d358", "Id": "735f5db5626147582d2ae3f2c87be8e5e697c088574c5faaf8d4d1bccab99470", "Created": 1386142123 },{ "VirtualSize": 662553464, "Size": 662553464, "RepoTags": [ ":" ], "ParentId": "", "Id": "4c1208b690c68af3476b437e7bc2bcc460f062bda2094d2d8f21a7e70368d358", "Created": 1386114144 }]`, regexps: []string{ `base -> "4c1208b690c6"`, @@ -60,6 +67,32 @@ func Test_Dot(t *testing.T) { } } +func Test_Tree(t *testing.T) { + treeTests := []TreeTest{ + TreeTest{ + json: `[{ "VirtualSize": 662553464, "Size": 0, "RepoTags": [ "foo:latest" ], "ParentId": "735f5db5626147582d2ae3f2c87be8e5e697c088574c5faaf8d4d1bccab99470", "Id": "c87be8e5e697c735f5db5626147582d2ae3f2088574c5faaf8d4d1bccab99470", "Created": 1386142123 },{ "VirtualSize": 662553464, "Size": 0, "RepoTags": [ ":" ], "ParentId": "4c1208b690c68af3476b437e7bc2bcc460f062bda2094d2d8f21a7e70368d358", "Id": "735f5db5626147582d2ae3f2c87be8e5e697c088574c5faaf8d4d1bccab99470", "Created": 1386142123 },{ "VirtualSize": 662553464, "Size": 662553464, "RepoTags": [ ":" ], "ParentId": "", "Id": "4c1208b690c68af3476b437e7bc2bcc460f062bda2094d2d8f21a7e70368d358", "Created": 1386114144 }]`, + startImage: "", + noTrunc: false, + regexps: []string{ + `(?m)└─4c1208b690c6`, + `(?m) └─735f5db56261`, + `(?m) └─c87be8e5e697`, + }, + }, + } + + for _, treeTest := range treeTests { + im, _ := parseJSON([]byte(treeTest.json)) + result := jsonToTree(im, treeTest.startImage, treeTest.noTrunc) + + for _, regexp := range compileRegexps(t, treeTest.regexps) { + if !regexp.MatchString(result) { + t.Fatalf("images tree content '%s' did not match regexp '%s'", result, regexp) + } + } + } +} + func compileRegexps(t *testing.T, regexpStrings []string) []*regexp.Regexp { compiledRegexps := []*regexp.Regexp{}