From dddb2e6b975e5efbf24e3ad91f9e5ab0aca42ba6 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 16 Oct 2018 22:38:41 -0400 Subject: [PATCH] with fancy pre-processing --- Makefile | 2 +- README.md | 10 ++-- cmd/analyze.go | 2 + image/image.go | 135 ++++++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 129 insertions(+), 20 deletions(-) diff --git a/Makefile b/Makefile index e79f5a2..744b28d 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ all: clean build run: build docker image ls | grep "dive-test" >/dev/null || docker build -t dive-test:latest . - ./build/$(BIN) die-test + ./build/$(BIN) dive-test build: go build -o build/$(BIN) diff --git a/README.md b/README.md index cc94dc0..95bc567 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ A tool for interrogating docker images. +**This is beta quality!** + + + +## Installing ``` docker build -t die-test:latest . go run main.go ``` -# TODO: - -- [ ] Diff trees - -- [ ] Add ui for diffing stack to layer diff --git a/cmd/analyze.go b/cmd/analyze.go index 33b3498..33c7ac1 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -6,6 +6,7 @@ import ( "os" "github.com/wagoodman/dive/image" "github.com/wagoodman/dive/ui" + "github.com/fatih/color" ) // analyze takes a docker image tag, digest, or id and displayes the @@ -16,6 +17,7 @@ func analyze(cmd *cobra.Command, args []string) { fmt.Println("No image argument given") os.Exit(1) } + color.New(color.Bold).Println("Analyzing Image") manifest, refTrees := image.InitializeData(userImage) ui.Run(manifest, refTrees) } \ No newline at end of file diff --git a/image/image.go b/image/image.go index ef09f71..4a6b6bf 100644 --- a/image/image.go +++ b/image/image.go @@ -15,14 +15,57 @@ import ( "github.com/docker/docker/client" "github.com/wagoodman/dive/filetree" "golang.org/x/net/context" + "github.com/wagoodman/jotframe" + "github.com/k0kubun/go-ansi" ) +// TODO: this file should be rethought... but since it's only for preprocessing it'll be tech debt for now. + func check(e error) { if e != nil { panic(e) } } +type ProgressBar struct { + percent int + rawTotal int64 + rawCurrent int64 +} + +func NewProgressBar(total int64) *ProgressBar{ + return &ProgressBar{ + rawTotal: total, + } +} + +func (pb *ProgressBar) Done() { + pb.rawCurrent = pb.rawTotal + pb.percent = 100 +} + +func (pb *ProgressBar) Update(currentValue int64) (hasChanged bool) { + pb.rawCurrent = currentValue + percent := int(100.0*(float64(pb.rawCurrent) / float64(pb.rawTotal))) + if percent != pb.percent { + hasChanged = true + } + pb.percent = percent + return hasChanged +} + +func (pb *ProgressBar) String() string { + width := 40 + done := int((pb.percent*width)/100.0) + todo := width - done + head := 1 + // if pb.percent >= 100 { + // head = 0 + // } + + return "[" + strings.Repeat("=", done) + strings.Repeat(">", head) + strings.Repeat(" ", todo) + "]" + fmt.Sprintf(" %d %% (%d/%d)", pb.percent, pb.rawCurrent, pb.rawTotal) +} + type ImageManifest struct { ConfigPath string `json:"Config"` RepoTags []string `json:"RepoTags"` @@ -92,7 +135,7 @@ func NewImageConfig(reader *tar.Reader, header *tar.Header) ImageConfig { func GetImageConfig(imageTarPath string, manifest ImageManifest) ImageConfig{ var config ImageConfig // read through the image contents and build a tree - fmt.Println("Fetching image config...") + fmt.Println(" Fetching image config...") tarFile, err := os.Open(imageTarPath) if err != nil { fmt.Println(err) @@ -123,16 +166,27 @@ func GetImageConfig(imageTarPath string, manifest ImageManifest) ImageConfig{ return config } -func processTar(layerMap map[string]*filetree.FileTree, name string, tarredBytes []byte) { +func processLayerTar(line *jotframe.Line, layerMap map[string]*filetree.FileTree, name string, tarredBytes []byte) { tree := filetree.NewFileTree() tree.Name = name fileInfos := getFileList(tarredBytes) - for _, element := range fileInfos { + + shortName := name[:15] + pb := NewProgressBar(int64(len(fileInfos))) + for idx, element := range fileInfos { tree.FileSize += uint64(element.TarHeader.FileInfo().Size()) tree.AddPath(element.Path, element) + + if pb.Update(int64(idx)) { + io.WriteString(line, fmt.Sprintf(" ├─ %s : %s", shortName, pb.String())) + } } + pb.Done() + io.WriteString(line, fmt.Sprintf(" ├─ %s : %s", shortName, pb.String())) + layerMap[tree.Name] = tree + line.Close() } func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) { @@ -140,16 +194,16 @@ func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) { var layerMap = make(map[string]*filetree.FileTree) var trees = make([]*filetree.FileTree, 0) + ansi.CursorHide() + // save this image to disk temporarily to get the content info - fmt.Println("Fetching image...") imageTarPath, tmpDir := saveImage(imageID) - // imageTarPath := "/tmp/dive229500681/image.tar" - // tmpDir := "/tmp/dive229500681" + // imageTarPath := "/tmp/dive516670682/image.tar" + // tmpDir := "/tmp/dive516670682" // fmt.Println(tmpDir) defer os.RemoveAll(tmpDir) // read through the image contents and build a tree - fmt.Printf("Reading image '%s'...\n", imageID) tarFile, err := os.Open(imageTarPath) if err != nil { fmt.Println(err) @@ -157,14 +211,25 @@ func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) { } defer tarFile.Close() + fi, err := tarFile.Stat() + if err != nil { + panic(err) + } + totalSize := fi.Size() + var observedBytes int64 + var percent int + tarReader := tar.NewReader(tarFile) + frame := jotframe.NewFixedFrame(1, true, false, false) + lastLine := frame.Lines()[0] + io.WriteString(lastLine, " ╧") + lastLine.Close() for { header, err := tarReader.Next() - // log.Debug(header.Name) - if err == io.EOF { + io.WriteString(frame.Header(), " Discovering layers... Done!") break } @@ -173,23 +238,33 @@ func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) { os.Exit(1) } + observedBytes += header.Size + percent = int(100.0*(float64(observedBytes) / float64(totalSize))) + io.WriteString(frame.Header(), fmt.Sprintf(" Discovering layers... %d %%", percent)) + name := header.Name switch header.Typeflag { case tar.TypeDir: continue case tar.TypeReg: - // todo: process this loop in parallel, visualize with jotframe if strings.HasSuffix(name, "layer.tar") { + line, err := frame.Prepend() + if err != nil { + panic(err) + } + shortName := name[:15] + io.WriteString(line, " ├─ " + shortName + " : loading...") var tarredBytes = make([]byte, header.Size) - _, err := tarReader.Read(tarredBytes) + _, err = tarReader.Read(tarredBytes) if err != nil && err != io.EOF { panic(err) } - go processTar(layerMap, name, tarredBytes) + + go processLayerTar(line, layerMap, name, tarredBytes) } else if name == "manifest.json" { manifest = NewImageManifest(tarReader, header) @@ -198,14 +273,17 @@ func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) { fmt.Printf("ERRG: unknown tar entry: %v: %s\n", header.Typeflag, name) } } + frame.Header().Close() + frame.Wait() + frame.Remove(lastLine) + fmt.Println("") // obtain the image history config := GetImageConfig(imageTarPath, manifest) // build the content tree - fmt.Println("Building tree...") + fmt.Println(" Building tree...") for _, treeName := range manifest.LayerTarPaths { - fmt.Printf("%s : %+v\n", treeName, layerMap[treeName]) trees = append(trees, layerMap[treeName]) } @@ -236,6 +314,8 @@ func InitializeData(imageID string) ([]*Layer, []*filetree.FileTree) { layerIdx-- } + ansi.CursorShow() + return layers, trees } @@ -246,6 +326,20 @@ func saveImage(imageID string) (string, string) { panic(err) } + frame := jotframe.NewFixedFrame(0, false, false, true) + line, err := frame.Append() + check(err) + io.WriteString(line, " Fetching metadata...") + + result, _, err := dockerClient.ImageInspectWithRaw(ctx, imageID) + check(err) + totalSize := result.Size + + frame.Remove(line) + line, err = frame.Append() + check(err) + io.WriteString(line, " Fetching image...") + readCloser, err := dockerClient.ImageSave(ctx, []string{imageID}) check(err) defer readCloser.Close() @@ -263,6 +357,9 @@ func saveImage(imageID string) (string, string) { } }() imageWriter := bufio.NewWriter(imageFile) + pb := NewProgressBar(totalSize) + + var observedBytes int64 buf := make([]byte, 1024) for { @@ -274,6 +371,12 @@ func saveImage(imageID string) (string, string) { break } + observedBytes += int64(n) + + if pb.Update(observedBytes) { + io.WriteString(line, fmt.Sprintf(" Fetching image... %s", pb.String())) + } + if _, err := imageWriter.Write(buf[:n]); err != nil { panic(err) } @@ -283,6 +386,10 @@ func saveImage(imageID string) (string, string) { panic(err) } + pb.Done() + io.WriteString(line, fmt.Sprintf(" Fetching image... %s", pb.String())) + frame.Close() + return imageTarPath, tmpDir }