From e67734d38d0641cae0ac052ba7d4b76ed0a39432 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 7 Jun 2018 15:51:10 -0400 Subject: [PATCH] Added debug panel; annotate filetree with changeinfo (#7) --- Makefile | 4 +- cmd/die/main.go | 33 +++++--- filetree/changeinfo.go | 24 ++++++ filetree/tree.go | 9 +- filetree/tree_test.go | 6 +- image/image.go | 183 ++++++++++++++++++++++++++++++----------- image/tar_read.go | 166 ------------------------------------- ui/filetreeview.go | 20 +++-- ui/layerview.go | 15 ++-- ui/ui.go | 18 ++-- 10 files changed, 220 insertions(+), 258 deletions(-) delete mode 100644 image/tar_read.go diff --git a/Makefile b/Makefile index 83b02ea..168a37c 100644 --- a/Makefile +++ b/Makefile @@ -3,9 +3,9 @@ BIN = die all: clean build run: build - ./build/$(BIN) + ./build/$(BIN) die-test -build: deps +build: #deps go build -o build/$(BIN) ./cmd/... install: deps diff --git a/cmd/die/main.go b/cmd/die/main.go index ab39ead..fa9b019 100644 --- a/cmd/die/main.go +++ b/cmd/die/main.go @@ -1,7 +1,11 @@ package main import ( + "fmt" + "log" "os" + + "github.com/urfave/cli" "github.com/wagoodman/docker-image-explorer/image" "github.com/wagoodman/docker-image-explorer/ui" ) @@ -11,15 +15,22 @@ const version = "v0.0.0" const author = "wagoodman" func main() { - os.Exit(run(os.Args)) + app := cli.NewApp() + app.Name = "die" + app.Usage = "Explore your docker images" + app.Action = func(c *cli.Context) error { + userImage := c.Args().Get(0) + if userImage == "" { + fmt.Println("No image argument given") + os.Exit(1) + } + manifest, refTrees := image.InitializeData(userImage) + ui.Run(manifest, refTrees) + return nil + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } } - - -func run(args []string) int { - image.WriteImage() - manifest, refTrees := image.InitializeData() - - ui.Run(manifest, refTrees) - return 0 -} - diff --git a/filetree/changeinfo.go b/filetree/changeinfo.go index f1b19ba..95a6529 100644 --- a/filetree/changeinfo.go +++ b/filetree/changeinfo.go @@ -1,8 +1,11 @@ package filetree import ( + "archive/tar" "bytes" + "crypto/md5" "fmt" + "io" ) type FileChangeInfo struct { @@ -22,6 +25,27 @@ const ( Removed ) +func NewFileChangeInfo(reader *tar.Reader, header *tar.Header, path string) FileChangeInfo { + if header.Typeflag == tar.TypeDir { + return FileChangeInfo{ + Path: path, + Typeflag: header.Typeflag, + MD5sum: [16]byte{}, + } + } + fileBytes := make([]byte, header.Size) + _, err := reader.Read(fileBytes) + if err != nil && err != io.EOF { + panic(err) + } + return FileChangeInfo{ + Path: path, + Typeflag: header.Typeflag, + MD5sum: md5.Sum(fileBytes), + DiffType: Unchanged, + } +} + func (d DiffType) String() string { switch d { case Unchanged: diff --git a/filetree/tree.go b/filetree/tree.go index d1efd1a..090bf4e 100644 --- a/filetree/tree.go +++ b/filetree/tree.go @@ -140,6 +140,7 @@ func (tree *FileTree) GetNode(path string) (*FileNode, error) { } func (tree *FileTree) AddPath(path string, data *FileChangeInfo) (*FileNode, error) { + // fmt.Printf("ADDPATH: %s %+v\n", path, data) nodeNames := strings.Split(path, "/") node := tree.Root for idx, name := range nodeNames { @@ -172,7 +173,7 @@ func (tree *FileTree) RemovePath(path string) error { return node.Remove() } -func (tree *FileTree) compare(upper *FileTree) error { +func (tree *FileTree) Compare(upper *FileTree) error { graft := func(node *FileNode) error { if node.IsWhiteout() { err := tree.MarkRemoved(node.Path()) @@ -183,14 +184,14 @@ func (tree *FileTree) compare(upper *FileTree) error { existingNode, _ := tree.GetNode(node.Path()) if existingNode == nil { newNode, err := tree.AddPath(node.Path(), node.Data) - fmt.Printf("added new node at %s\n", newNode.Path()) + // fmt.Printf("added new node at %s\n", newNode.Path()) if err != nil { return fmt.Errorf("Cannot add new node %s: %v", node.Path(), err.Error()) } newNode.AssignDiffType(Added) } else { diffType := existingNode.compare(node) - fmt.Printf("found existing node at %s\n", existingNode.Path()) + // fmt.Printf("found existing node at %s\n", existingNode.Path()) existingNode.deriveDiffType(diffType) } } @@ -209,7 +210,7 @@ func (tree *FileTree) MarkRemoved(path string) error { func StackRange(trees []*FileTree, index uint) *FileTree { tree := trees[1].Copy() - for idx := uint(2); idx < index; idx++ { + for idx := uint(1); idx <= index; idx++ { tree.Stack(trees[idx]) } return tree diff --git a/filetree/tree_test.go b/filetree/tree_test.go index 67d6c2e..14cb61b 100644 --- a/filetree/tree_test.go +++ b/filetree/tree_test.go @@ -186,7 +186,7 @@ func TestCompareWithNoChanges(t *testing.T) { lowerTree.AddPath(value, &fakeData) upperTree.AddPath(value, &fakeData) } - lowerTree.compare(upperTree) + lowerTree.Compare(upperTree) asserter := func(n *FileNode) error { if n.Path() == "/" { return nil @@ -232,7 +232,7 @@ func TestCompareWithAdds(t *testing.T) { upperTree.AddPath(value, &fakeData) } - lowerTree.compare(upperTree) + lowerTree.Compare(upperTree) asserter := func(n *FileNode) error { p := n.Path() @@ -283,7 +283,7 @@ func TestCompareWithChanges(t *testing.T) { upperTree.AddPath(value, &fakeData) } - lowerTree.compare(upperTree) + lowerTree.Compare(upperTree) asserter := func(n *FileNode) error { p := n.Path() if p == "/" { diff --git a/image/image.go b/image/image.go index 76617a1..ad6e745 100644 --- a/image/image.go +++ b/image/image.go @@ -1,12 +1,19 @@ package image import ( - "io" - "os" + "archive/tar" "bufio" - "github.com/docker/docker/client" - "fmt" + "bytes" "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/docker/docker/client" + "github.com/wagoodman/docker-image-explorer/filetree" "golang.org/x/net/context" ) @@ -16,24 +23,112 @@ func check(e error) { } } +type ImageManifest struct { + Config string + RepoTags []string + Layers []string +} -func saveImage(readCloser io.ReadCloser) { - defer readCloser.Close() +func NewManifest(reader *tar.Reader, header *tar.Header) ImageManifest { + size := header.Size + manifestBytes := make([]byte, size) + _, err := reader.Read(manifestBytes) + if err != nil { + panic(err) + } + var m []ImageManifest + err = json.Unmarshal(manifestBytes, &m) + if err != nil { + panic(err) + } + return m[0] +} - path := ".image" - if _, err := os.Stat(path); os.IsNotExist(err) { - os.Mkdir(path, 0755) +func InitializeData(imageID string) (*ImageManifest, []*filetree.FileTree) { + imageTarPath, tmpDir := saveImage(imageID) + + f, err := os.Open(imageTarPath) + if err != nil { + fmt.Println(err) + os.Exit(1) } - fo, err := os.Create(".image/cache.tar") + defer f.Close() + defer os.RemoveAll(tmpDir) + + tarReader := tar.NewReader(f) + targetName := "manifest.json" + var manifest ImageManifest + var layerMap map[string]*filetree.FileTree + layerMap = make(map[string]*filetree.FileTree) + + for { + header, err := tarReader.Next() + + if err == io.EOF { + break + } + + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + name := header.Name + if name == targetName { + manifest = NewManifest(tarReader, header) + } + + switch header.Typeflag { + case tar.TypeDir: + continue + case tar.TypeReg: + if strings.HasSuffix(name, "layer.tar") { + tree := filetree.NewFileTree() + tree.Name = name + fileInfos := getFileList(tarReader, header) + for _, element := range fileInfos { + tree.AddPath(element.Path, &element) + } + layerMap[tree.Name] = tree + } + default: + fmt.Printf("ERRG: unknown tar entry: %v: %s\n", header.Typeflag, name) + } + } + var trees []*filetree.FileTree + trees = make([]*filetree.FileTree, 0) + for _, treeName := range manifest.Layers { + trees = append(trees, layerMap[treeName]) + } + + return &manifest, trees +} + +func saveImage(imageID string) (string, string) { + ctx := context.Background() + dockerClient, err := client.NewEnvClient() + if err != nil { + panic(err) + } + + readCloser, err := dockerClient.ImageSave(ctx, []string{imageID}) + check(err) + defer readCloser.Close() + + tmpDir, err := ioutil.TempDir("", "docker-image-explorer") + check(err) + + imageTarPath := filepath.Join(tmpDir, "image.tar") + imageFile, err := os.Create(imageTarPath) check(err) defer func() { - if err := fo.Close(); err != nil { + if err := imageFile.Close(); err != nil { panic(err) } }() - w := bufio.NewWriter(fo) + imageWriter := bufio.NewWriter(imageFile) buf := make([]byte, 1024) for { @@ -45,60 +140,50 @@ func saveImage(readCloser io.ReadCloser) { break } - if _, err := w.Write(buf[:n]); err != nil { + if _, err := imageWriter.Write(buf[:n]); err != nil { panic(err) } } - if err = w.Flush(); err != nil { + if err = imageWriter.Flush(); err != nil { panic(err) } + + return imageTarPath, tmpDir } - -func WriteImage() { - ctx := context.Background() - cli, err := client.NewEnvClient() +func getFileList(parentReader *tar.Reader, h *tar.Header) []filetree.FileChangeInfo { + var files []filetree.FileChangeInfo + size := h.Size + tarredBytes := make([]byte, size) + _, err := parentReader.Read(tarredBytes) if err != nil { panic(err) } - - // imageID := "golang:alpine" - imageID := "die-test:latest" - - fmt.Println("Saving Image...") - readCloser, err := cli.ImageSave(ctx, []string{imageID}) - check(err) - saveImage(readCloser) - + r := bytes.NewReader(tarredBytes) + tarReader := tar.NewReader(r) for { - inspect, _, err := cli.ImageInspectWithRaw(ctx, imageID) - check(err) + header, err := tarReader.Next() - history, err := cli.ImageHistory(ctx, imageID) - check(err) - - historyStr, err := json.MarshalIndent(history, "", " ") - check(err) - - layerStr := "" - for idx, layer := range inspect.RootFS.Layers { - prefix := "├── " - if idx == len(inspect.RootFS.Layers)-1 { - prefix = "└── " - } - layerStr += fmt.Sprintf("%s%s\n", prefix, layer) + if err == io.EOF { + break } - fmt.Printf("Image: %s\nId: %s\nParent: %s\nLayers: %d\n%sHistory: %s\n", imageID, inspect.ID, inspect.Parent, len(inspect.RootFS.Layers), layerStr, historyStr) + if err != nil { + fmt.Println(err) + os.Exit(1) + } - fmt.Println("") + name := header.Name - if inspect.Parent == "" { - break - } else { - imageID = inspect.Parent + switch header.Typeflag { + case tar.TypeXGlobalHeader: + fmt.Printf("ERRG: XGlobalHeader: %v: %s\n", header.Typeflag, name) + case tar.TypeXHeader: + fmt.Printf("ERRG: XHeader: %v: %s\n", header.Typeflag, name) + default: + files = append(files, filetree.NewFileChangeInfo(tarReader, header, name)) } } - fmt.Println("See './.image' for the cached image tar") + return files } diff --git a/image/tar_read.go b/image/tar_read.go deleted file mode 100644 index 7b9cca5..0000000 --- a/image/tar_read.go +++ /dev/null @@ -1,166 +0,0 @@ -package image - -import ( - "archive/tar" - "bytes" - "crypto/md5" - "encoding/json" - "fmt" - "io" - "os" - "strings" - - "github.com/wagoodman/docker-image-explorer/filetree" -) - -func InitializeData() (*Manifest, []*filetree.FileTree) { - f, err := os.Open("./.image/cache.tar") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - defer f.Close() - - tarReader := tar.NewReader(f) - targetName := "manifest.json" - var manifest Manifest - var layerMap map[string]*filetree.FileTree - layerMap = make(map[string]*filetree.FileTree) - - for { - header, err := tarReader.Next() - - if err == io.EOF { - break - } - - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - name := header.Name - if name == targetName { - manifest = handleManifest(tarReader, header) - } - - switch header.Typeflag { - case tar.TypeDir: - continue - case tar.TypeReg: - - if strings.HasSuffix(name, "layer.tar") { - fmt.Println("Containing:") - tree := filetree.NewFileTree() - tree.Name = name - fmt.Printf("%s\n", tree.Name) - fileInfos := getFileList(tarReader, header) - for _, element := range fileInfos { - tree.AddPath(element.Path, &element) - } - layerMap[tree.Name] = tree - } - default: - fmt.Printf("%s : %c %s %s\n", - "hmmm?", - header.Typeflag, - "in file", - name, - ) - } - } - var trees []*filetree.FileTree - trees = make([]*filetree.FileTree, 0) - for _, treeName := range manifest.Layers { - trees = append(trees, layerMap[treeName]) - } - - return &manifest, trees -} - -func getFileList(parentReader *tar.Reader, h *tar.Header) []filetree.FileChangeInfo { - var files []filetree.FileChangeInfo - size := h.Size - tarredBytes := make([]byte, size) - _, err := parentReader.Read(tarredBytes) - if err != nil { - panic(err) - } - r := bytes.NewReader(tarredBytes) - tarReader := tar.NewReader(r) - for { - header, err := tarReader.Next() - - if err == io.EOF { - break - } - - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - name := header.Name - - switch header.Typeflag { - case tar.TypeDir: - files = append(files, makeEntry(tarReader, header, name)) - case tar.TypeReg: - files = append(files, makeEntry(tarReader, header, name)) - continue - case tar.TypeSymlink: - files = append(files, makeEntry(tarReader, header, name)) - default: - fmt.Printf("%s : %c %s %s\n", - "hmmm?", - header.Typeflag, - "in file", - name, - ) - } - } - return files -} - -func makeEntry(r *tar.Reader, h *tar.Header, path string) filetree.FileChangeInfo { - if h.Typeflag == tar.TypeDir { - return filetree.FileChangeInfo{ - Path: path, - Typeflag: h.Typeflag, - MD5sum: [16]byte{}, - } - } - fileBytes := make([]byte, h.Size) - _, err := r.Read(fileBytes) - if err != nil && err != io.EOF { - panic(err) - } - hash := md5.Sum(fileBytes) - return filetree.FileChangeInfo{ - Path: path, - Typeflag: h.Typeflag, - MD5sum: hash, - DiffType: filetree.Unchanged, - } -} - -type Manifest struct { - Config string - RepoTags []string - Layers []string -} - -func handleManifest(r *tar.Reader, header *tar.Header) Manifest { - size := header.Size - manifestBytes := make([]byte, size) - _, err := r.Read(manifestBytes) - if err != nil { - panic(err) - } - var m [1]Manifest - err = json.Unmarshal(manifestBytes, &m) - if err != nil { - panic(err) - } - return m[0] -} diff --git a/ui/filetreeview.go b/ui/filetreeview.go index 138c796..685b1d2 100644 --- a/ui/filetreeview.go +++ b/ui/filetreeview.go @@ -7,14 +7,13 @@ import ( "github.com/wagoodman/docker-image-explorer/filetree" ) - type FileTreeView struct { - Name string - gui *gocui.Gui - view *gocui.View + Name string + gui *gocui.Gui + view *gocui.View TreeIndex uint - Tree *filetree.FileTree - RefTrees []*filetree.FileTree + Tree *filetree.FileTree + RefTrees []*filetree.FileTree } func NewFileTreeView(name string, gui *gocui.Gui, tree *filetree.FileTree, refTrees []*filetree.FileTree) (treeview *FileTreeView) { @@ -55,9 +54,12 @@ func (view *FileTreeView) Setup(v *gocui.View) error { return nil } -// Mehh, this is just a bad method -func (view *FileTreeView) reset(tree *filetree.FileTree) error { - view.Tree = tree +func (view *FileTreeView) setLayer(layerIndex uint) error { + view.Tree = filetree.StackRange(view.RefTrees, layerIndex-1) + view.Tree.Compare(view.RefTrees[layerIndex]) + v, _ := view.gui.View("debug") + v.Clear() + _, _ = fmt.Fprintln(v, view.RefTrees[layerIndex]) view.view.SetCursor(0, 0) view.TreeIndex = 0 return view.Render() diff --git a/ui/layerview.go b/ui/layerview.go index 281e20d..b8dd08e 100644 --- a/ui/layerview.go +++ b/ui/layerview.go @@ -5,19 +5,17 @@ import ( "github.com/jroimartin/gocui" "github.com/wagoodman/docker-image-explorer/image" - "github.com/wagoodman/docker-image-explorer/filetree" ) - type LayerView struct { Name string gui *gocui.Gui view *gocui.View LayerIndex uint - Manifest *image.Manifest + Manifest *image.ImageManifest } -func NewLayerView(name string, gui *gocui.Gui, manifest *image.Manifest) (layerview *LayerView) { +func NewLayerView(name string, gui *gocui.Gui, manifest *image.ImageManifest) (layerview *LayerView) { layerview = new(LayerView) // populate main fields @@ -38,10 +36,10 @@ func (view *LayerView) Setup(v *gocui.View) error { view.view.SelFgColor = gocui.ColorBlack // set keybindings - if err := view.gui.SetKeybinding("side", gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorDown() }); err != nil { + if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorDown() }); err != nil { return err } - if err := view.gui.SetKeybinding("side", gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorUp() }); err != nil { + if err := view.gui.SetKeybinding(view.Name, gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error { return view.CursorUp() }); err != nil { return err } @@ -67,8 +65,7 @@ func (view *LayerView) CursorDown() error { CursorDown(view.gui, view.view) view.LayerIndex++ view.Render() - // this line is evil - Views.Tree.reset(filetree.StackRange(Views.Tree.RefTrees, view.LayerIndex)) + Views.Tree.setLayer(view.LayerIndex) } return nil } @@ -79,7 +76,7 @@ func (view *LayerView) CursorUp() error { view.LayerIndex-- view.Render() // this line is evil - Views.Tree.reset(filetree.StackRange(Views.Tree.RefTrees, view.LayerIndex)) + Views.Tree.setLayer(view.LayerIndex) } return nil } diff --git a/ui/ui.go b/ui/ui.go index 48321ce..bdbc897 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -1,9 +1,10 @@ package ui import ( + "log" + "github.com/jroimartin/gocui" "github.com/wagoodman/docker-image-explorer/filetree" - "log" "github.com/wagoodman/docker-image-explorer/image" ) @@ -76,14 +77,15 @@ func keybindings(g *gocui.Gui) error { func layout(g *gocui.Gui) error { maxX, maxY := g.Size() splitCol := 50 - if view, err := g.SetView("side", -1, -1, splitCol, maxY); err != nil { + debugCol := maxX - 100 + if view, err := g.SetView(Views.Layer.Name, -1, -1, splitCol, maxY); err != nil { if err != gocui.ErrUnknownView { return err } Views.Layer.Setup(view) } - if view, err := g.SetView("main", splitCol, -1, maxX, maxY); err != nil { + if view, err := g.SetView(Views.Tree.Name, splitCol, -1, debugCol, maxY); err != nil { if err != gocui.ErrUnknownView { return err } @@ -94,10 +96,16 @@ func layout(g *gocui.Gui) error { return err } } + if _, err := g.SetView("debug", debugCol, -1, maxX, maxY); err != nil { + if err != gocui.ErrUnknownView { + return err + } + } + return nil } -func Run(manifest *image.Manifest, refTrees []*filetree.FileTree) { +func Run(manifest *image.ImageManifest, refTrees []*filetree.FileTree) { g, err := gocui.NewGui(gocui.OutputNormal) if err != nil { @@ -119,4 +127,4 @@ func Run(manifest *image.Manifest, refTrees []*filetree.FileTree) { if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { log.Panicln(err) } -} \ No newline at end of file +}