Added debug panel; annotate filetree with changeinfo (#7)

This commit is contained in:
Alex Goodman 2018-06-07 15:51:10 -04:00 committed by GitHub
parent acec670354
commit e67734d38d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 220 additions and 258 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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:

View File

@ -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

View File

@ -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 == "/" {

View File

@ -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
}

View File

@ -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]
}

View File

@ -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()

View File

@ -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
}

View File

@ -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)
}
}
}