From 93509843547d17b9eb4a01b54c915928c2dac53d Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 24 May 2018 21:22:52 -0400 Subject: [PATCH] added goals; refactored to filetree --- README.md | 11 +++- filetree.go | 136 ------------------------------------------- filetree_test.go | 141 --------------------------------------------- tar-read.go | 2 +- tree.go | 146 +++++++++++++++++++++++++++++++++++++++++++---- tree_test.go | 139 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 284 insertions(+), 291 deletions(-) delete mode 100644 filetree.go delete mode 100644 filetree_test.go diff --git a/README.md b/README.md index 9be128a..3bb8538 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,13 @@ ``` docker build -t die-test:latest . go run main.go -``` \ No newline at end of file +``` + +# TODO: + +[x] Extract docker layers from api +[x] Represent layers as generic tree +[x] Stack ordere tree list together as fake unionfs tree +[ ] Diff trees +[ ] Add ui for browsing layers +[ ] Add ui for diffing stack to layer diff --git a/filetree.go b/filetree.go deleted file mode 100644 index d92a598..0000000 --- a/filetree.go +++ /dev/null @@ -1,136 +0,0 @@ -package main - -import ( - "errors" - "strings" - "fmt" -) - -type FileTree interface { - AddPath(string, interface{}) *Node - RemovePath(string) error - Visit(Visiter) error - // Diff(*Tree) error - Stack(*Tree) (Tree, error) -} - -type Visiter func(*Node) error - -func (tree *Tree) Visit(visiter Visiter) error { - return tree.root.Visit(visiter) -} - -func (node *Node) Visit(visiter Visiter) error { - for _, child := range node.children { - err := child.Visit(visiter) - if err != nil { - return err - } - } - return visiter(node) -} - -func (node *Node) IsWhiteout() bool { - return strings.HasPrefix(node.name, ".wh.") -} - -func (node *Node) Path() string { - path := []string{} - curNode := node - for { - if curNode.parent == nil{ - break - } - path = append([]string{curNode.name}, path...) - curNode = curNode.parent - } - return "/" + strings.Join(path, "/") -} - - -func (node *Node) WhiteoutPath() string { - path := []string{} - curNode := node - for { - if curNode.parent == nil{ - break - } - - name := curNode.name - if curNode == node { - name = strings.TrimPrefix(name, ".wh.") - } - - path = append([]string{name}, path...) - curNode = curNode.parent - } - return "/" + strings.Join(path, "/") -} - - - -func (tree *Tree) Stack(upper *Tree) (error) { - graft := func(node *Node) error { - if node.IsWhiteout() { - err := tree.RemovePath(node.WhiteoutPath()) - if err != nil { - return fmt.Errorf("Cannot remove node %s: %v", node.Path(), err.Error()) - } - } else { - newNode, err := tree.AddPath(node.Path(), node.data) - if err != nil { - return fmt.Errorf("Cannot add node %s: %v", newNode.Path(), err.Error()) - } - } - return nil - } - return upper.Visit(graft) -} - -func (tree *Tree) GetNode(path string) (*Node, error) { - nodeNames := strings.Split(path, "/") - node := tree.Root() - for _, name := range nodeNames { - if name == "" { - continue - } - if node.children[name] == nil { - return nil, errors.New("Path does not exist") - } - node = node.children[name] - } - return node, nil -} - -func (tree *Tree) AddPath(path string, data interface{}) (*Node, error) { - nodeNames := strings.Split(path, "/") - node := tree.Root() - for idx, name := range nodeNames { - if name == "" { - continue - } - // find or create node - if node.children[name] != nil { - node = node.children[name] - } else { - // don't attach the payload. The payload is destined for the - // path's end node, not any intermediary node. - node = node.AddChild(name, nil) - } - - // attach payload to the last specified node - if idx == len(nodeNames)-1 { - node.data = data - } - - } - return node, nil -} - -func (tree *Tree) RemovePath(path string) error { - node, err := tree.GetNode(path) - if err != nil { - return err - } - return node.Remove() -} diff --git a/filetree_test.go b/filetree_test.go deleted file mode 100644 index 9280759..0000000 --- a/filetree_test.go +++ /dev/null @@ -1,141 +0,0 @@ -package main - -import "testing" - -func TestAddPath(t *testing.T) { - tree := NewTree() - tree.AddPath("/etc/nginx/nginx.conf", 1) - tree.AddPath("/etc/nginx/public", 2) - tree.AddPath("/var/run/systemd", 3) - tree.AddPath("/var/run/bashful", 4) - tree.AddPath("/tmp", 5) - tree.AddPath("/tmp/nonsense", 6) - - expected := `. -├── etc -│ └── nginx -│ ├── nginx.conf -│ └── public -├── tmp -│ └── nonsense -└── var - └── run - ├── bashful - └── systemd -` - actual := tree.String() - - if expected != actual { - t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual) - } - -} - -func TestRemovePath(t *testing.T) { - tree := NewTree() - tree.AddPath("/etc/nginx/nginx.conf", 1) - tree.AddPath("/etc/nginx/public", 2) - tree.AddPath("/var/run/systemd", 3) - tree.AddPath("/var/run/bashful", 4) - tree.AddPath("/tmp", 5) - tree.AddPath("/tmp/nonsense", 6) - - tree.RemovePath("/var/run/bashful") - tree.RemovePath("/tmp") - - expected := `. -├── etc -│ └── nginx -│ ├── nginx.conf -│ └── public -└── var - └── run - └── systemd -` - actual := tree.String() - - if expected != actual { - t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual) - } - -} - -func TestPath(t *testing.T) { - expected := "/etc/nginx/nginx.conf" - tree := NewTree() - node, _ := tree.AddPath(expected, nil) - - actual := node.Path() - if expected != actual { - t.Errorf("Expected path '%s' got '%s'", expected, actual) - } -} - -func TestIsWhiteout(t *testing.T) { - tree1 := NewTree() - p1, _ := tree1.AddPath("/etc/nginx/public1", 2) - p2, _ := tree1.AddPath("/etc/nginx/.wh.public2", 2) - - if p1.IsWhiteout() != false { - t.Errorf("Expected path '%s' to **not** be a whiteout file", p1.name) - } - - if p2.IsWhiteout() != true { - t.Errorf("Expected path '%s' to be a whiteout file", p2.name) - } -} - - -func TestStack(t *testing.T) { - payloadKey := "/var/run/systemd" - payloadValue := 1263487 - - tree1 := NewTree() - - tree1.AddPath("/etc/nginx/public", 2) - tree1.AddPath(payloadKey, 3) - tree1.AddPath("/var/run/bashful", 4) - tree1.AddPath("/tmp", 5) - tree1.AddPath("/tmp/nonsense", 6) - - tree2 := NewTree() - // add new files - tree2.AddPath("/etc/nginx/nginx.conf", 1) - // modify current files - tree2.AddPath(payloadKey, payloadValue) - // whiteout the following files - tree2.AddPath("/var/run/.wh.bashful", nil) - tree2.AddPath("/.wh.tmp", nil) - - err := tree1.Stack(tree2) - - if err != nil { - t.Errorf("Could not stack trees: %v", err) - } - - expected := `. -├── etc -│ └── nginx -│ ├── nginx.conf -│ └── public -└── var - └── run - └── systemd -` - - node, err := tree1.GetNode(payloadKey) - if err != nil { - t.Errorf("Expected '%s' to still exist, but it doesn't", payloadKey) - } - - if node.data != payloadValue { - t.Errorf("Expected '%s' value to be %d but got %d", payloadKey, payloadValue, node.data.(int)) - } - - actual := tree1.String() - - if expected != actual { - t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual) - } - -} \ No newline at end of file diff --git a/tar-read.go b/tar-read.go index 98b5333..aefd406 100644 --- a/tar-read.go +++ b/tar-read.go @@ -28,7 +28,7 @@ func main() { tarReader := tar.NewReader(f) targetName := "manifest.json" var m Manifest - var trees []*Tree + var trees []*FileTree for { header, err := tarReader.Next() diff --git a/tree.go b/tree.go index ebcf817..cec4b6e 100644 --- a/tree.go +++ b/tree.go @@ -2,32 +2,45 @@ package main import ( "sort" + "strings" + "fmt" + "errors" ) const ( - newLine = "\n" - emptySpace = " " - middleItem = "├── " - continueItem = "│ " - lastItem = "└── " + newLine = "\n" + emptySpace = " " + middleItem = "├── " + continueItem = "│ " + lastItem = "└── " + whiteoutPrefix = ".wh." ) -type Tree struct { +//type FileTree interface { +// AddPath(string, interface{}) *Node +// RemovePath(string) error +// Visit(Visiter) error +// // Diff(*FileTree) error +// Stack(*FileTree) (FileTree, error) +//} + + +type FileTree struct { root *Node size int name string } type Node struct { - tree *Tree + tree *FileTree parent *Node name string data interface{} children map[string]*Node } -func NewTree() (tree *Tree) { - tree = new(Tree) +func NewTree() (tree *FileTree) { + tree = new(FileTree) tree.size = 0 tree.root = new(Node) tree.root.tree = tree @@ -45,7 +58,7 @@ func NewNode(parent *Node, name string, data interface{}) (node *Node) { return node } -func (tree *Tree) Root() *Node { +func (tree *FileTree) Root() *Node { return tree.root } @@ -74,7 +87,7 @@ func (node *Node) String() string { return node.name } -func (tree *Tree) String() string { +func (tree *FileTree) String() string { var renderLine func(string, []bool, bool) string var walkTree func(*Node, []bool) string @@ -129,7 +142,7 @@ func (node *Node) Copy() *Node { return newNode } -func (tree *Tree) Copy() *Tree { +func (tree *FileTree) Copy() *FileTree { newTree := NewTree() *newTree = *tree newTree.root = tree.Root().Copy() @@ -137,3 +150,112 @@ func (tree *Tree) Copy() *Tree { return newTree } +type Visiter func(*Node) error + +func (tree *FileTree) Visit(visiter Visiter) error { + return tree.root.Visit(visiter) +} + +func (node *Node) Visit(visiter Visiter) error { + for _, child := range node.children { + err := child.Visit(visiter) + if err != nil { + return err + } + } + return visiter(node) +} + +func (node *Node) IsWhiteout() bool { + return strings.HasPrefix(node.name, whiteoutPrefix) +} + +func (node *Node) Path() string { + path := []string{} + curNode := node + for { + if curNode.parent == nil{ + break + } + + name := curNode.name + if curNode == node { + // white out prefixes are fictitious on leaf nodes + name = strings.TrimPrefix(name, whiteoutPrefix) + } + + path = append([]string{name}, path...) + curNode = curNode.parent + } + return "/" + strings.Join(path, "/") +} + + + +func (tree *FileTree) Stack(upper *FileTree) (error) { + graft := func(node *Node) error { + if node.IsWhiteout() { + err := tree.RemovePath(node.Path()) + if err != nil { + return fmt.Errorf("Cannot remove node %s: %v", node.Path(), err.Error()) + } + } else { + newNode, err := tree.AddPath(node.Path(), node.data) + if err != nil { + return fmt.Errorf("Cannot add node %s: %v", newNode.Path(), err.Error()) + } + } + return nil + } + return upper.Visit(graft) +} + +func (tree *FileTree) GetNode(path string) (*Node, error) { + nodeNames := strings.Split(path, "/") + node := tree.Root() + for _, name := range nodeNames { + if name == "" { + continue + } + if node.children[name] == nil { + return nil, errors.New("Path does not exist") + } + node = node.children[name] + } + return node, nil +} + +func (tree *FileTree) AddPath(path string, data interface{}) (*Node, error) { + nodeNames := strings.Split(path, "/") + node := tree.Root() + for idx, name := range nodeNames { + if name == "" { + continue + } + // find or create node + if node.children[name] != nil { + node = node.children[name] + } else { + // don't attach the payload. The payload is destined for the + // path's end node, not any intermediary node. + node = node.AddChild(name, nil) + } + + // attach payload to the last specified node + if idx == len(nodeNames)-1 { + node.data = data + } + + } + return node, nil +} + +func (tree *FileTree) RemovePath(path string) error { + node, err := tree.GetNode(path) + if err != nil { + return err + } + return node.Remove() +} + + diff --git a/tree_test.go b/tree_test.go index b143789..e6355f5 100644 --- a/tree_test.go +++ b/tree_test.go @@ -95,3 +95,142 @@ func TestPrintTree(t *testing.T) { } } + + +func TestAddPath(t *testing.T) { + tree := NewTree() + tree.AddPath("/etc/nginx/nginx.conf", 1) + tree.AddPath("/etc/nginx/public", 2) + tree.AddPath("/var/run/systemd", 3) + tree.AddPath("/var/run/bashful", 4) + tree.AddPath("/tmp", 5) + tree.AddPath("/tmp/nonsense", 6) + + expected := `. +├── etc +│ └── nginx +│ ├── nginx.conf +│ └── public +├── tmp +│ └── nonsense +└── var + └── run + ├── bashful + └── systemd +` + actual := tree.String() + + if expected != actual { + t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual) + } + +} + +func TestRemovePath(t *testing.T) { + tree := NewTree() + tree.AddPath("/etc/nginx/nginx.conf", 1) + tree.AddPath("/etc/nginx/public", 2) + tree.AddPath("/var/run/systemd", 3) + tree.AddPath("/var/run/bashful", 4) + tree.AddPath("/tmp", 5) + tree.AddPath("/tmp/nonsense", 6) + + tree.RemovePath("/var/run/bashful") + tree.RemovePath("/tmp") + + expected := `. +├── etc +│ └── nginx +│ ├── nginx.conf +│ └── public +└── var + └── run + └── systemd +` + actual := tree.String() + + if expected != actual { + t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual) + } + +} + +func TestPath(t *testing.T) { + expected := "/etc/nginx/nginx.conf" + tree := NewTree() + node, _ := tree.AddPath(expected, nil) + + actual := node.Path() + if expected != actual { + t.Errorf("Expected path '%s' got '%s'", expected, actual) + } +} + +func TestIsWhiteout(t *testing.T) { + tree1 := NewTree() + p1, _ := tree1.AddPath("/etc/nginx/public1", 2) + p2, _ := tree1.AddPath("/etc/nginx/.wh.public2", 2) + + if p1.IsWhiteout() != false { + t.Errorf("Expected path '%s' to **not** be a whiteout file", p1.name) + } + + if p2.IsWhiteout() != true { + t.Errorf("Expected path '%s' to be a whiteout file", p2.name) + } +} + + +func TestStack(t *testing.T) { + payloadKey := "/var/run/systemd" + payloadValue := 1263487 + + tree1 := NewTree() + + tree1.AddPath("/etc/nginx/public", 2) + tree1.AddPath(payloadKey, 3) + tree1.AddPath("/var/run/bashful", 4) + tree1.AddPath("/tmp", 5) + tree1.AddPath("/tmp/nonsense", 6) + + tree2 := NewTree() + // add new files + tree2.AddPath("/etc/nginx/nginx.conf", 1) + // modify current files + tree2.AddPath(payloadKey, payloadValue) + // whiteout the following files + tree2.AddPath("/var/run/.wh.bashful", nil) + tree2.AddPath("/.wh.tmp", nil) + + err := tree1.Stack(tree2) + + if err != nil { + t.Errorf("Could not stack trees: %v", err) + } + + expected := `. +├── etc +│ └── nginx +│ ├── nginx.conf +│ └── public +└── var + └── run + └── systemd +` + + node, err := tree1.GetNode(payloadKey) + if err != nil { + t.Errorf("Expected '%s' to still exist, but it doesn't", payloadKey) + } + + if node.data != payloadValue { + t.Errorf("Expected '%s' value to be %d but got %d", payloadKey, payloadValue, node.data.(int)) + } + + actual := tree1.String() + + if expected != actual { + t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual) + } + +} \ No newline at end of file