rework package structure
This commit is contained in:
parent
4b73bc89b8
commit
576709ad30
2
Makefile
2
Makefile
@ -30,7 +30,7 @@ test-coverage: build
|
|||||||
./.scripts/test-coverage.sh
|
./.scripts/test-coverage.sh
|
||||||
|
|
||||||
validate:
|
validate:
|
||||||
grep -R 'const allowTestDataCapture = false' ui/
|
grep -R 'const allowTestDataCapture = false' runtime/ui/
|
||||||
go vet ./...
|
go vet ./...
|
||||||
@! gofmt -s -l . 2>&1 | grep -vE '^\.git/' | grep -vE '^\.cache/'
|
@! gofmt -s -l . 2>&1 | grep -vE '^\.git/' | grep -vE '^\.cache/'
|
||||||
golangci-lint run
|
golangci-lint run
|
||||||
|
@ -2,6 +2,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/wagoodman/dive/dive/filetree"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@ -11,7 +12,6 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"github.com/wagoodman/dive/filetree"
|
|
||||||
"github.com/wagoodman/dive/utils"
|
"github.com/wagoodman/dive/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,79 +0,0 @@
|
|||||||
package filetree
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TreeCacheKey struct {
|
|
||||||
bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int
|
|
||||||
}
|
|
||||||
|
|
||||||
type TreeCache struct {
|
|
||||||
refTrees []*FileTree
|
|
||||||
cache map[TreeCacheKey]*FileTree
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cache *TreeCache) Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int) *FileTree {
|
|
||||||
key := TreeCacheKey{bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop}
|
|
||||||
if value, exists := cache.cache[key]; exists {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
value := cache.buildTree(key)
|
|
||||||
cache.cache[key] = value
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cache *TreeCache) buildTree(key TreeCacheKey) *FileTree {
|
|
||||||
newTree := StackTreeRange(cache.refTrees, key.bottomTreeStart, key.bottomTreeStop)
|
|
||||||
for idx := key.topTreeStart; idx <= key.topTreeStop; idx++ {
|
|
||||||
err := newTree.CompareAndMark(cache.refTrees[idx])
|
|
||||||
if err != nil {
|
|
||||||
logrus.Errorf("unable to build tree: %+v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newTree
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cache *TreeCache) Build() {
|
|
||||||
var bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop int
|
|
||||||
|
|
||||||
// case 1: layer compare (top tree SIZE is fixed (BUT floats forward), Bottom tree SIZE changes)
|
|
||||||
for selectIdx := 0; selectIdx < len(cache.refTrees); selectIdx++ {
|
|
||||||
bottomTreeStart = 0
|
|
||||||
topTreeStop = selectIdx
|
|
||||||
|
|
||||||
if selectIdx == 0 {
|
|
||||||
bottomTreeStop = selectIdx
|
|
||||||
topTreeStart = selectIdx
|
|
||||||
} else {
|
|
||||||
bottomTreeStop = selectIdx - 1
|
|
||||||
topTreeStart = selectIdx
|
|
||||||
}
|
|
||||||
|
|
||||||
cache.Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
|
|
||||||
}
|
|
||||||
|
|
||||||
// case 2: aggregated compare (bottom tree is ENTIRELY fixed, top tree SIZE changes)
|
|
||||||
for selectIdx := 0; selectIdx < len(cache.refTrees); selectIdx++ {
|
|
||||||
bottomTreeStart = 0
|
|
||||||
topTreeStop = selectIdx
|
|
||||||
if selectIdx == 0 {
|
|
||||||
bottomTreeStop = selectIdx
|
|
||||||
topTreeStart = selectIdx
|
|
||||||
} else {
|
|
||||||
bottomTreeStop = 0
|
|
||||||
topTreeStart = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
cache.Get(bottomTreeStart, bottomTreeStop, topTreeStart, topTreeStop)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFileTreeCache(refTrees []*FileTree) TreeCache {
|
|
||||||
|
|
||||||
return TreeCache{
|
|
||||||
refTrees: refTrees,
|
|
||||||
cache: make(map[TreeCacheKey]*FileTree),
|
|
||||||
}
|
|
||||||
}
|
|
161
filetree/data.go
161
filetree/data.go
@ -1,161 +0,0 @@
|
|||||||
package filetree
|
|
||||||
|
|
||||||
import (
|
|
||||||
"archive/tar"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/cespare/xxhash"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
Unmodified DiffType = iota
|
|
||||||
Modified
|
|
||||||
Added
|
|
||||||
Removed
|
|
||||||
)
|
|
||||||
|
|
||||||
var GlobalFileTreeCollapse bool
|
|
||||||
|
|
||||||
// NewNodeData creates an empty NodeData struct for a FileNode
|
|
||||||
func NewNodeData() *NodeData {
|
|
||||||
return &NodeData{
|
|
||||||
ViewInfo: *NewViewInfo(),
|
|
||||||
FileInfo: FileInfo{},
|
|
||||||
DiffType: Unmodified,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy duplicates a NodeData
|
|
||||||
func (data *NodeData) Copy() *NodeData {
|
|
||||||
return &NodeData{
|
|
||||||
ViewInfo: *data.ViewInfo.Copy(),
|
|
||||||
FileInfo: *data.FileInfo.Copy(),
|
|
||||||
DiffType: data.DiffType,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewViewInfo creates a default ViewInfo
|
|
||||||
func NewViewInfo() (view *ViewInfo) {
|
|
||||||
return &ViewInfo{
|
|
||||||
Collapsed: GlobalFileTreeCollapse,
|
|
||||||
Hidden: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy duplicates a ViewInfo
|
|
||||||
func (view *ViewInfo) Copy() (newView *ViewInfo) {
|
|
||||||
newView = NewViewInfo()
|
|
||||||
*newView = *view
|
|
||||||
return newView
|
|
||||||
}
|
|
||||||
|
|
||||||
func getHashFromReader(reader io.Reader) uint64 {
|
|
||||||
h := xxhash.New()
|
|
||||||
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
for {
|
|
||||||
n, err := reader.Read(buf)
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
logrus.Panic(err)
|
|
||||||
}
|
|
||||||
if n == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = h.Write(buf[:n])
|
|
||||||
if err != nil {
|
|
||||||
logrus.Panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.Sum64()
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFileInfo extracts the metadata from a tar header and file contents and generates a new FileInfo object.
|
|
||||||
func NewFileInfo(reader *tar.Reader, header *tar.Header, path string) FileInfo {
|
|
||||||
if header.Typeflag == tar.TypeDir {
|
|
||||||
return FileInfo{
|
|
||||||
Path: path,
|
|
||||||
TypeFlag: header.Typeflag,
|
|
||||||
Linkname: header.Linkname,
|
|
||||||
hash: 0,
|
|
||||||
Size: header.FileInfo().Size(),
|
|
||||||
Mode: header.FileInfo().Mode(),
|
|
||||||
Uid: header.Uid,
|
|
||||||
Gid: header.Gid,
|
|
||||||
IsDir: header.FileInfo().IsDir(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hash := getHashFromReader(reader)
|
|
||||||
|
|
||||||
return FileInfo{
|
|
||||||
Path: path,
|
|
||||||
TypeFlag: header.Typeflag,
|
|
||||||
Linkname: header.Linkname,
|
|
||||||
hash: hash,
|
|
||||||
Size: header.FileInfo().Size(),
|
|
||||||
Mode: header.FileInfo().Mode(),
|
|
||||||
Uid: header.Uid,
|
|
||||||
Gid: header.Gid,
|
|
||||||
IsDir: header.FileInfo().IsDir(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy duplicates a FileInfo
|
|
||||||
func (data *FileInfo) Copy() *FileInfo {
|
|
||||||
if data == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &FileInfo{
|
|
||||||
Path: data.Path,
|
|
||||||
TypeFlag: data.TypeFlag,
|
|
||||||
Linkname: data.Linkname,
|
|
||||||
hash: data.hash,
|
|
||||||
Size: data.Size,
|
|
||||||
Mode: data.Mode,
|
|
||||||
Uid: data.Uid,
|
|
||||||
Gid: data.Gid,
|
|
||||||
IsDir: data.IsDir,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare determines the DiffType between two FileInfos based on the type and contents of each given FileInfo
|
|
||||||
func (data *FileInfo) Compare(other FileInfo) DiffType {
|
|
||||||
if data.TypeFlag == other.TypeFlag {
|
|
||||||
if data.hash == other.hash &&
|
|
||||||
data.Mode == other.Mode &&
|
|
||||||
data.Uid == other.Uid &&
|
|
||||||
data.Gid == other.Gid {
|
|
||||||
return Unmodified
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Modified
|
|
||||||
}
|
|
||||||
|
|
||||||
// String of a DiffType
|
|
||||||
func (diff DiffType) String() string {
|
|
||||||
switch diff {
|
|
||||||
case Unmodified:
|
|
||||||
return "Unmodified"
|
|
||||||
case Modified:
|
|
||||||
return "Modified"
|
|
||||||
case Added:
|
|
||||||
return "Added"
|
|
||||||
case Removed:
|
|
||||||
return "Removed"
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("%d", int(diff))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// merge two DiffTypes into a single result. Essentially, return the given value unless they two values differ,
|
|
||||||
// in which case we can only determine that there is "a change".
|
|
||||||
func (diff DiffType) merge(other DiffType) DiffType {
|
|
||||||
if diff == other {
|
|
||||||
return diff
|
|
||||||
}
|
|
||||||
return Modified
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
package filetree
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAssignDiffType(t *testing.T) {
|
|
||||||
tree := NewFileTree()
|
|
||||||
node, _, err := tree.AddPath("/usr", *BlankFileChangeInfo("/usr"))
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected no error from fetching path. got: %v", err)
|
|
||||||
}
|
|
||||||
node.Data.DiffType = Modified
|
|
||||||
if tree.Root.Children["usr"].Data.DiffType != Modified {
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMergeDiffTypes(t *testing.T) {
|
|
||||||
a := Unmodified
|
|
||||||
b := Unmodified
|
|
||||||
merged := a.merge(b)
|
|
||||||
if merged != Unmodified {
|
|
||||||
t.Errorf("Expected Unchaged (0) but got %v", merged)
|
|
||||||
}
|
|
||||||
a = Modified
|
|
||||||
b = Unmodified
|
|
||||||
merged = a.merge(b)
|
|
||||||
if merged != Modified {
|
|
||||||
t.Errorf("Expected Unchaged (0) but got %v", merged)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BlankFileChangeInfo(path string) (f *FileInfo) {
|
|
||||||
result := FileInfo{
|
|
||||||
Path: path,
|
|
||||||
TypeFlag: 1,
|
|
||||||
hash: 123,
|
|
||||||
}
|
|
||||||
return &result
|
|
||||||
}
|
|
@ -1,111 +0,0 @@
|
|||||||
package filetree
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Len is required for sorting.
|
|
||||||
func (efs EfficiencySlice) Len() int {
|
|
||||||
return len(efs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Swap operation is required for sorting.
|
|
||||||
func (efs EfficiencySlice) Swap(i, j int) {
|
|
||||||
efs[i], efs[j] = efs[j], efs[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Less comparison is required for sorting.
|
|
||||||
func (efs EfficiencySlice) Less(i, j int) bool {
|
|
||||||
return efs[i].CumulativeSize < efs[j].CumulativeSize
|
|
||||||
}
|
|
||||||
|
|
||||||
// Efficiency returns the score and file set of the given set of FileTrees (layers). This is loosely based on:
|
|
||||||
// 1. Files that are duplicated across layers discounts your score, weighted by file size
|
|
||||||
// 2. Files that are removed discounts your score, weighted by the original file size
|
|
||||||
func Efficiency(trees []*FileTree) (float64, EfficiencySlice) {
|
|
||||||
efficiencyMap := make(map[string]*EfficiencyData)
|
|
||||||
inefficientMatches := make(EfficiencySlice, 0)
|
|
||||||
currentTree := 0
|
|
||||||
|
|
||||||
visitor := func(node *FileNode) error {
|
|
||||||
path := node.Path()
|
|
||||||
if _, ok := efficiencyMap[path]; !ok {
|
|
||||||
efficiencyMap[path] = &EfficiencyData{
|
|
||||||
Path: path,
|
|
||||||
Nodes: make([]*FileNode, 0),
|
|
||||||
minDiscoveredSize: -1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data := efficiencyMap[path]
|
|
||||||
|
|
||||||
// this node may have had children that were deleted, however, we won't explicitly list out every child, only
|
|
||||||
// the top-most parent with the cumulative size. These operations will need to be done on the full (stacked)
|
|
||||||
// tree.
|
|
||||||
// Note: whiteout files may also represent directories, so we need to find out if this was previously a file or dir.
|
|
||||||
var sizeBytes int64
|
|
||||||
|
|
||||||
if node.IsWhiteout() {
|
|
||||||
sizer := func(curNode *FileNode) error {
|
|
||||||
sizeBytes += curNode.Data.FileInfo.Size
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
stackedTree := StackTreeRange(trees, 0, currentTree-1)
|
|
||||||
previousTreeNode, err := stackedTree.GetNode(node.Path())
|
|
||||||
if err != nil {
|
|
||||||
logrus.Debug(fmt.Sprintf("CurrentTree: %d : %s", currentTree, err))
|
|
||||||
} else if previousTreeNode.Data.FileInfo.IsDir {
|
|
||||||
err = previousTreeNode.VisitDepthChildFirst(sizer, nil)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Errorf("unable to propagate whiteout dir: %+v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
sizeBytes = node.Data.FileInfo.Size
|
|
||||||
}
|
|
||||||
|
|
||||||
data.CumulativeSize += sizeBytes
|
|
||||||
if data.minDiscoveredSize < 0 || sizeBytes < data.minDiscoveredSize {
|
|
||||||
data.minDiscoveredSize = sizeBytes
|
|
||||||
}
|
|
||||||
data.Nodes = append(data.Nodes, node)
|
|
||||||
|
|
||||||
if len(data.Nodes) == 2 {
|
|
||||||
inefficientMatches = append(inefficientMatches, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
visitEvaluator := func(node *FileNode) bool {
|
|
||||||
return node.IsLeaf()
|
|
||||||
}
|
|
||||||
for idx, tree := range trees {
|
|
||||||
currentTree = idx
|
|
||||||
err := tree.VisitDepthChildFirst(visitor, visitEvaluator)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Errorf("unable to propagate ref tree: %+v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate the score
|
|
||||||
var minimumPathSizes int64
|
|
||||||
var discoveredPathSizes int64
|
|
||||||
|
|
||||||
for _, value := range efficiencyMap {
|
|
||||||
minimumPathSizes += value.minDiscoveredSize
|
|
||||||
discoveredPathSizes += value.CumulativeSize
|
|
||||||
}
|
|
||||||
var score float64
|
|
||||||
if discoveredPathSizes == 0 {
|
|
||||||
score = 1.0
|
|
||||||
} else {
|
|
||||||
score = float64(minimumPathSizes) / float64(discoveredPathSizes)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Sort(inefficientMatches)
|
|
||||||
|
|
||||||
return score, inefficientMatches
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
package filetree
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func checkError(t *testing.T, err error, message string) {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf(message+": %+v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEfficency(t *testing.T) {
|
|
||||||
trees := make([]*FileTree, 3)
|
|
||||||
for idx := range trees {
|
|
||||||
trees[idx] = NewFileTree()
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _, err := trees[0].AddPath("/etc/nginx/nginx.conf", FileInfo{Size: 2000})
|
|
||||||
checkError(t, err, "could not setup test")
|
|
||||||
|
|
||||||
_, _, err = trees[0].AddPath("/etc/nginx/public", FileInfo{Size: 3000})
|
|
||||||
checkError(t, err, "could not setup test")
|
|
||||||
|
|
||||||
_, _, err = trees[1].AddPath("/etc/nginx/nginx.conf", FileInfo{Size: 5000})
|
|
||||||
checkError(t, err, "could not setup test")
|
|
||||||
_, _, err = trees[1].AddPath("/etc/athing", FileInfo{Size: 10000})
|
|
||||||
checkError(t, err, "could not setup test")
|
|
||||||
|
|
||||||
_, _, err = trees[2].AddPath("/etc/.wh.nginx", *BlankFileChangeInfo("/etc/.wh.nginx"))
|
|
||||||
checkError(t, err, "could not setup test")
|
|
||||||
|
|
||||||
var expectedScore = 0.75
|
|
||||||
var expectedMatches = EfficiencySlice{
|
|
||||||
&EfficiencyData{Path: "/etc/nginx/nginx.conf", CumulativeSize: 7000},
|
|
||||||
}
|
|
||||||
actualScore, actualMatches := Efficiency(trees)
|
|
||||||
|
|
||||||
if expectedScore != actualScore {
|
|
||||||
t.Errorf("Expected score of %v but go %v", expectedScore, actualScore)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(actualMatches) != len(expectedMatches) {
|
|
||||||
for _, match := range actualMatches {
|
|
||||||
t.Logf(" match: %+v", match)
|
|
||||||
}
|
|
||||||
t.Fatalf("Expected to find %d inefficient paths, but found %d", len(expectedMatches), len(actualMatches))
|
|
||||||
}
|
|
||||||
|
|
||||||
if expectedMatches[0].Path != actualMatches[0].Path {
|
|
||||||
t.Errorf("Expected path of %s but go %s", expectedMatches[0].Path, actualMatches[0].Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
if expectedMatches[0].CumulativeSize != actualMatches[0].CumulativeSize {
|
|
||||||
t.Errorf("Expected cumulative size of %v but go %v", expectedMatches[0].CumulativeSize, actualMatches[0].CumulativeSize)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEfficency_ScratchImage(t *testing.T) {
|
|
||||||
trees := make([]*FileTree, 3)
|
|
||||||
for idx := range trees {
|
|
||||||
trees[idx] = NewFileTree()
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _, err := trees[0].AddPath("/nothing", FileInfo{Size: 0})
|
|
||||||
checkError(t, err, "could not setup test")
|
|
||||||
|
|
||||||
var expectedScore = 1.0
|
|
||||||
var expectedMatches = EfficiencySlice{}
|
|
||||||
actualScore, actualMatches := Efficiency(trees)
|
|
||||||
|
|
||||||
if expectedScore != actualScore {
|
|
||||||
t.Errorf("Expected score of %v but go %v", expectedScore, actualScore)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(actualMatches) > 0 {
|
|
||||||
t.Fatalf("Expected to find %d inefficient paths, but found %d", len(expectedMatches), len(actualMatches))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
315
filetree/node.go
315
filetree/node.go
@ -1,315 +0,0 @@
|
|||||||
package filetree
|
|
||||||
|
|
||||||
import (
|
|
||||||
"archive/tar"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
|
||||||
"github.com/fatih/color"
|
|
||||||
"github.com/phayes/permbits"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
AttributeFormat = "%s%s %11s %10s "
|
|
||||||
)
|
|
||||||
|
|
||||||
var diffTypeColor = map[DiffType]*color.Color{
|
|
||||||
Added: color.New(color.FgGreen),
|
|
||||||
Removed: color.New(color.FgRed),
|
|
||||||
Modified: color.New(color.FgYellow),
|
|
||||||
Unmodified: color.New(color.Reset),
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewNode creates a new FileNode relative to the given parent node with a payload.
|
|
||||||
func NewNode(parent *FileNode, name string, data FileInfo) (node *FileNode) {
|
|
||||||
node = new(FileNode)
|
|
||||||
node.Name = name
|
|
||||||
node.Data = *NewNodeData()
|
|
||||||
node.Data.FileInfo = *data.Copy()
|
|
||||||
|
|
||||||
node.Children = make(map[string]*FileNode)
|
|
||||||
node.Parent = parent
|
|
||||||
if parent != nil {
|
|
||||||
node.Tree = parent.Tree
|
|
||||||
}
|
|
||||||
|
|
||||||
return node
|
|
||||||
}
|
|
||||||
|
|
||||||
// renderTreeLine returns a string representing this FileNode in the context of a greater ASCII tree.
|
|
||||||
func (node *FileNode) renderTreeLine(spaces []bool, last bool, collapsed bool) string {
|
|
||||||
var otherBranches string
|
|
||||||
for _, space := range spaces {
|
|
||||||
if space {
|
|
||||||
otherBranches += noBranchSpace
|
|
||||||
} else {
|
|
||||||
otherBranches += branchSpace
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
thisBranch := middleItem
|
|
||||||
if last {
|
|
||||||
thisBranch = lastItem
|
|
||||||
}
|
|
||||||
|
|
||||||
collapsedIndicator := uncollapsedItem
|
|
||||||
if collapsed {
|
|
||||||
collapsedIndicator = collapsedItem
|
|
||||||
}
|
|
||||||
|
|
||||||
return otherBranches + thisBranch + collapsedIndicator + node.String() + newLine
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy duplicates the existing node relative to a new parent node.
|
|
||||||
func (node *FileNode) Copy(parent *FileNode) *FileNode {
|
|
||||||
newNode := NewNode(parent, node.Name, node.Data.FileInfo)
|
|
||||||
newNode.Data.ViewInfo = node.Data.ViewInfo
|
|
||||||
newNode.Data.DiffType = node.Data.DiffType
|
|
||||||
for name, child := range node.Children {
|
|
||||||
newNode.Children[name] = child.Copy(newNode)
|
|
||||||
child.Parent = newNode
|
|
||||||
}
|
|
||||||
return newNode
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddChild creates a new node relative to the current FileNode.
|
|
||||||
func (node *FileNode) AddChild(name string, data FileInfo) (child *FileNode) {
|
|
||||||
// never allow processing of purely whiteout flag files (for now)
|
|
||||||
if strings.HasPrefix(name, doubleWhiteoutPrefix) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
child = NewNode(node, name, data)
|
|
||||||
if node.Children[name] != nil {
|
|
||||||
// tree node already exists, replace the payload, keep the children
|
|
||||||
node.Children[name].Data.FileInfo = *data.Copy()
|
|
||||||
} else {
|
|
||||||
node.Children[name] = child
|
|
||||||
node.Tree.Size++
|
|
||||||
}
|
|
||||||
|
|
||||||
return child
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove deletes the current FileNode from it's parent FileNode's relations.
|
|
||||||
func (node *FileNode) Remove() error {
|
|
||||||
if node == node.Tree.Root {
|
|
||||||
return fmt.Errorf("cannot remove the tree root")
|
|
||||||
}
|
|
||||||
for _, child := range node.Children {
|
|
||||||
err := child.Remove()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete(node.Parent.Children, node.Name)
|
|
||||||
node.Tree.Size--
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// String shows the filename formatted into the proper color (by DiffType), additionally indicating if it is a symlink.
|
|
||||||
func (node *FileNode) String() string {
|
|
||||||
var display string
|
|
||||||
if node == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
display = node.Name
|
|
||||||
if node.Data.FileInfo.TypeFlag == tar.TypeSymlink || node.Data.FileInfo.TypeFlag == tar.TypeLink {
|
|
||||||
display += " → " + node.Data.FileInfo.Linkname
|
|
||||||
}
|
|
||||||
return diffTypeColor[node.Data.DiffType].Sprint(display)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MetadatString returns the FileNode metadata in a columnar string.
|
|
||||||
func (node *FileNode) MetadataString() string {
|
|
||||||
if node == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
fileMode := permbits.FileMode(node.Data.FileInfo.Mode).String()
|
|
||||||
dir := "-"
|
|
||||||
if node.Data.FileInfo.IsDir {
|
|
||||||
dir = "d"
|
|
||||||
}
|
|
||||||
user := node.Data.FileInfo.Uid
|
|
||||||
group := node.Data.FileInfo.Gid
|
|
||||||
userGroup := fmt.Sprintf("%d:%d", user, group)
|
|
||||||
|
|
||||||
var sizeBytes int64
|
|
||||||
|
|
||||||
if node.IsLeaf() {
|
|
||||||
sizeBytes = node.Data.FileInfo.Size
|
|
||||||
} else {
|
|
||||||
sizer := func(curNode *FileNode) error {
|
|
||||||
// don't include file sizes of children that have been removed (unless the node in question is a removed dir,
|
|
||||||
// then show the accumulated size of removed files)
|
|
||||||
if curNode.Data.DiffType != Removed || node.Data.DiffType == Removed {
|
|
||||||
sizeBytes += curNode.Data.FileInfo.Size
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := node.VisitDepthChildFirst(sizer, nil)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Errorf("unable to propagate node for metadata: %+v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
size := humanize.Bytes(uint64(sizeBytes))
|
|
||||||
|
|
||||||
return diffTypeColor[node.Data.DiffType].Sprint(fmt.Sprintf(AttributeFormat, dir, fileMode, userGroup, size))
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitDepthChildFirst iterates a tree depth-first (starting at this FileNode), evaluating the deepest depths first (visit on bubble up)
|
|
||||||
func (node *FileNode) VisitDepthChildFirst(visitor Visitor, evaluator VisitEvaluator) error {
|
|
||||||
var keys []string
|
|
||||||
for key := range node.Children {
|
|
||||||
keys = append(keys, key)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
for _, name := range keys {
|
|
||||||
child := node.Children[name]
|
|
||||||
err := child.VisitDepthChildFirst(visitor, evaluator)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// never visit the root node
|
|
||||||
if node == node.Tree.Root {
|
|
||||||
return nil
|
|
||||||
} else if evaluator != nil && evaluator(node) || evaluator == nil {
|
|
||||||
return visitor(node)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitDepthParentFirst iterates a tree depth-first (starting at this FileNode), evaluating the shallowest depths first (visit while sinking down)
|
|
||||||
func (node *FileNode) VisitDepthParentFirst(visitor Visitor, evaluator VisitEvaluator) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
doVisit := evaluator != nil && evaluator(node) || evaluator == nil
|
|
||||||
|
|
||||||
if !doVisit {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// never visit the root node
|
|
||||||
if node != node.Tree.Root {
|
|
||||||
err = visitor(node)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var keys []string
|
|
||||||
for key := range node.Children {
|
|
||||||
keys = append(keys, key)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
for _, name := range keys {
|
|
||||||
child := node.Children[name]
|
|
||||||
err = child.VisitDepthParentFirst(visitor, evaluator)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsWhiteout returns an indication if this file may be a overlay-whiteout file.
|
|
||||||
func (node *FileNode) IsWhiteout() bool {
|
|
||||||
return strings.HasPrefix(node.Name, whiteoutPrefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsLeaf returns true is the current node has no child nodes.
|
|
||||||
func (node *FileNode) IsLeaf() bool {
|
|
||||||
return len(node.Children) == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Path returns a slash-delimited string from the root of the greater tree to the current node (e.g. /a/path/to/here)
|
|
||||||
func (node *FileNode) Path() string {
|
|
||||||
if node.path == "" {
|
|
||||||
var 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
|
|
||||||
}
|
|
||||||
node.path = "/" + strings.Join(path, "/")
|
|
||||||
}
|
|
||||||
return strings.Replace(node.path, "//", "/", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// deriveDiffType determines a DiffType to the current FileNode. Note: the DiffType of a node is always the DiffType of
|
|
||||||
// its attributes and its contents. The contents are the bytes of the file of the children of a directory.
|
|
||||||
func (node *FileNode) deriveDiffType(diffType DiffType) error {
|
|
||||||
if node.IsLeaf() {
|
|
||||||
return node.AssignDiffType(diffType)
|
|
||||||
}
|
|
||||||
|
|
||||||
myDiffType := diffType
|
|
||||||
for _, v := range node.Children {
|
|
||||||
myDiffType = myDiffType.merge(v.Data.DiffType)
|
|
||||||
}
|
|
||||||
|
|
||||||
return node.AssignDiffType(myDiffType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssignDiffType will assign the given DiffType to this node, possibly affecting child nodes.
|
|
||||||
func (node *FileNode) AssignDiffType(diffType DiffType) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
node.Data.DiffType = diffType
|
|
||||||
|
|
||||||
if diffType == Removed {
|
|
||||||
// if we've removed this node, then all children have been removed as well
|
|
||||||
for _, child := range node.Children {
|
|
||||||
err = child.AssignDiffType(diffType)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// compare the current node against the given node, returning a definitive DiffType.
|
|
||||||
func (node *FileNode) compare(other *FileNode) DiffType {
|
|
||||||
if node == nil && other == nil {
|
|
||||||
return Unmodified
|
|
||||||
}
|
|
||||||
|
|
||||||
if node == nil && other != nil {
|
|
||||||
return Added
|
|
||||||
}
|
|
||||||
|
|
||||||
if node != nil && other == nil {
|
|
||||||
return Removed
|
|
||||||
}
|
|
||||||
|
|
||||||
if other.IsWhiteout() {
|
|
||||||
return Removed
|
|
||||||
}
|
|
||||||
if node.Name != other.Name {
|
|
||||||
panic("comparing mismatched nodes")
|
|
||||||
}
|
|
||||||
|
|
||||||
return node.Data.FileInfo.Compare(other.Data.FileInfo)
|
|
||||||
}
|
|
@ -1,168 +0,0 @@
|
|||||||
package filetree
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAddChild(t *testing.T) {
|
|
||||||
var expected, actual int
|
|
||||||
tree := NewFileTree()
|
|
||||||
|
|
||||||
payload := FileInfo{
|
|
||||||
Path: "stufffffs",
|
|
||||||
}
|
|
||||||
|
|
||||||
one := tree.Root.AddChild("first node!", payload)
|
|
||||||
|
|
||||||
two := tree.Root.AddChild("nil node!", FileInfo{})
|
|
||||||
|
|
||||||
tree.Root.AddChild("third node!", FileInfo{})
|
|
||||||
two.AddChild("forth, one level down...", FileInfo{})
|
|
||||||
two.AddChild("fifth, one level down...", FileInfo{})
|
|
||||||
two.AddChild("fifth, one level down...", FileInfo{})
|
|
||||||
|
|
||||||
expected, actual = 5, tree.Size
|
|
||||||
if expected != actual {
|
|
||||||
t.Errorf("Expected a tree size of %d got %d.", expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected, actual = 2, len(two.Children)
|
|
||||||
if expected != actual {
|
|
||||||
t.Errorf("Expected 'twos' number of children to be %d got %d.", expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected, actual = 3, len(tree.Root.Children)
|
|
||||||
if expected != actual {
|
|
||||||
t.Errorf("Expected 'twos' number of children to be %d got %d.", expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedFC := FileInfo{
|
|
||||||
Path: "stufffffs",
|
|
||||||
}
|
|
||||||
actualFC := one.Data.FileInfo
|
|
||||||
if expectedFC.Path != actualFC.Path {
|
|
||||||
t.Errorf("Expected 'ones' payload to be %+v got %+v.", expectedFC, actualFC)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemoveChild(t *testing.T) {
|
|
||||||
var expected, actual int
|
|
||||||
|
|
||||||
tree := NewFileTree()
|
|
||||||
tree.Root.AddChild("first", FileInfo{})
|
|
||||||
two := tree.Root.AddChild("nil", FileInfo{})
|
|
||||||
tree.Root.AddChild("third", FileInfo{})
|
|
||||||
forth := two.AddChild("forth", FileInfo{})
|
|
||||||
two.AddChild("fifth", FileInfo{})
|
|
||||||
|
|
||||||
err := forth.Remove()
|
|
||||||
checkError(t, err, "unable to setup test")
|
|
||||||
|
|
||||||
expected, actual = 4, tree.Size
|
|
||||||
if expected != actual {
|
|
||||||
t.Errorf("Expected a tree size of %d got %d.", expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tree.Root.Children["forth"] != nil {
|
|
||||||
t.Errorf("Expected 'forth' node to be deleted.")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = two.Remove()
|
|
||||||
checkError(t, err, "unable to setup test")
|
|
||||||
|
|
||||||
expected, actual = 2, tree.Size
|
|
||||||
if expected != actual {
|
|
||||||
t.Errorf("Expected a tree size of %d got %d.", expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tree.Root.Children["nil"] != nil {
|
|
||||||
t.Errorf("Expected 'nil' node to be deleted.")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPath(t *testing.T) {
|
|
||||||
expected := "/etc/nginx/nginx.conf"
|
|
||||||
tree := NewFileTree()
|
|
||||||
node, _, _ := tree.AddPath(expected, FileInfo{})
|
|
||||||
|
|
||||||
actual := node.Path()
|
|
||||||
if expected != actual {
|
|
||||||
t.Errorf("Expected path '%s' got '%s'", expected, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsWhiteout(t *testing.T) {
|
|
||||||
tree1 := NewFileTree()
|
|
||||||
p1, _, _ := tree1.AddPath("/etc/nginx/public1", FileInfo{})
|
|
||||||
p2, _, _ := tree1.AddPath("/etc/nginx/.wh.public2", FileInfo{})
|
|
||||||
p3, _, _ := tree1.AddPath("/etc/nginx/public3/.wh..wh..opq", FileInfo{})
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p3 != nil {
|
|
||||||
t.Errorf("Expected to not be able to add path '%s'", p2.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDiffTypeFromAddedChildren(t *testing.T) {
|
|
||||||
tree := NewFileTree()
|
|
||||||
node, _, _ := tree.AddPath("/usr", *BlankFileChangeInfo("/usr"))
|
|
||||||
node.Data.DiffType = Unmodified
|
|
||||||
|
|
||||||
node, _, _ = tree.AddPath("/usr/bin", *BlankFileChangeInfo("/usr/bin"))
|
|
||||||
node.Data.DiffType = Added
|
|
||||||
|
|
||||||
node, _, _ = tree.AddPath("/usr/bin2", *BlankFileChangeInfo("/usr/bin2"))
|
|
||||||
node.Data.DiffType = Removed
|
|
||||||
|
|
||||||
err := tree.Root.Children["usr"].deriveDiffType(Unmodified)
|
|
||||||
checkError(t, err, "unable to setup test")
|
|
||||||
|
|
||||||
if tree.Root.Children["usr"].Data.DiffType != Modified {
|
|
||||||
t.Errorf("Expected Modified but got %v", tree.Root.Children["usr"].Data.DiffType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func TestDiffTypeFromRemovedChildren(t *testing.T) {
|
|
||||||
tree := NewFileTree()
|
|
||||||
_, _, _ = tree.AddPath("/usr", *BlankFileChangeInfo("/usr"))
|
|
||||||
|
|
||||||
info1 := BlankFileChangeInfo("/usr/.wh.bin")
|
|
||||||
node, _, _ := tree.AddPath("/usr/.wh.bin", *info1)
|
|
||||||
node.Data.DiffType = Removed
|
|
||||||
|
|
||||||
info2 := BlankFileChangeInfo("/usr/.wh.bin2")
|
|
||||||
node, _, _ = tree.AddPath("/usr/.wh.bin2", *info2)
|
|
||||||
node.Data.DiffType = Removed
|
|
||||||
|
|
||||||
err := tree.Root.Children["usr"].deriveDiffType(Unmodified)
|
|
||||||
checkError(t, err, "unable to setup test")
|
|
||||||
|
|
||||||
if tree.Root.Children["usr"].Data.DiffType != Modified {
|
|
||||||
t.Errorf("Expected Modified but got %v", tree.Root.Children["usr"].Data.DiffType)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDirSize(t *testing.T) {
|
|
||||||
tree1 := NewFileTree()
|
|
||||||
_, _, err := tree1.AddPath("/etc/nginx/public1", FileInfo{Size: 100})
|
|
||||||
checkError(t, err, "unable to setup test")
|
|
||||||
_, _, err = tree1.AddPath("/etc/nginx/thing1", FileInfo{Size: 200})
|
|
||||||
checkError(t, err, "unable to setup test")
|
|
||||||
_, _, err = tree1.AddPath("/etc/nginx/public3/thing2", FileInfo{Size: 300})
|
|
||||||
checkError(t, err, "unable to setup test")
|
|
||||||
|
|
||||||
node, _ := tree1.GetNode("/etc/nginx")
|
|
||||||
expected, actual := "---------- 0:0 600 B ", node.MetadataString()
|
|
||||||
if expected != actual {
|
|
||||||
t.Errorf("Expected metadata '%s' got '%s'", expected, actual)
|
|
||||||
}
|
|
||||||
}
|
|
371
filetree/tree.go
371
filetree/tree.go
@ -1,371 +0,0 @@
|
|||||||
package filetree
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
newLine = "\n"
|
|
||||||
noBranchSpace = " "
|
|
||||||
branchSpace = "│ "
|
|
||||||
middleItem = "├─"
|
|
||||||
lastItem = "└─"
|
|
||||||
whiteoutPrefix = ".wh."
|
|
||||||
doubleWhiteoutPrefix = ".wh..wh.."
|
|
||||||
uncollapsedItem = "─ "
|
|
||||||
collapsedItem = "⊕ "
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewFileTree creates an empty FileTree
|
|
||||||
func NewFileTree() (tree *FileTree) {
|
|
||||||
tree = new(FileTree)
|
|
||||||
tree.Size = 0
|
|
||||||
tree.Root = new(FileNode)
|
|
||||||
tree.Root.Tree = tree
|
|
||||||
tree.Root.Children = make(map[string]*FileNode)
|
|
||||||
tree.Id = uuid.New()
|
|
||||||
return tree
|
|
||||||
}
|
|
||||||
|
|
||||||
// renderParams is a representation of a FileNode in the context of the greater tree. All
|
|
||||||
// data stored is necessary for rendering a single line in a tree format.
|
|
||||||
type renderParams struct {
|
|
||||||
node *FileNode
|
|
||||||
spaces []bool
|
|
||||||
childSpaces []bool
|
|
||||||
showCollapsed bool
|
|
||||||
isLast bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// renderStringTreeBetween returns a string representing the given tree between the given rows. Since each node
|
|
||||||
// is rendered on its own line, the returned string shows the visible nodes not affected by a collapsed parent.
|
|
||||||
func (tree *FileTree) renderStringTreeBetween(startRow, stopRow int, showAttributes bool) string {
|
|
||||||
// generate a list of nodes to render
|
|
||||||
var params = make([]renderParams, 0)
|
|
||||||
var result string
|
|
||||||
|
|
||||||
// visit from the front of the list
|
|
||||||
var paramsToVisit = []renderParams{{node: tree.Root, spaces: []bool{}, showCollapsed: false, isLast: false}}
|
|
||||||
for currentRow := 0; len(paramsToVisit) > 0 && currentRow <= stopRow; currentRow++ {
|
|
||||||
// pop the first node
|
|
||||||
var currentParams renderParams
|
|
||||||
currentParams, paramsToVisit = paramsToVisit[0], paramsToVisit[1:]
|
|
||||||
|
|
||||||
// take note of the next nodes to visit later
|
|
||||||
var keys []string
|
|
||||||
for key := range currentParams.node.Children {
|
|
||||||
keys = append(keys, key)
|
|
||||||
}
|
|
||||||
// we should always visit nodes in order
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
var childParams = make([]renderParams, 0)
|
|
||||||
for idx, name := range keys {
|
|
||||||
child := currentParams.node.Children[name]
|
|
||||||
// don't visit this node...
|
|
||||||
if child.Data.ViewInfo.Hidden || currentParams.node.Data.ViewInfo.Collapsed {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// visit this node...
|
|
||||||
isLast := idx == (len(currentParams.node.Children) - 1)
|
|
||||||
showCollapsed := child.Data.ViewInfo.Collapsed && len(child.Children) > 0
|
|
||||||
|
|
||||||
// completely copy the reference slice
|
|
||||||
childSpaces := make([]bool, len(currentParams.childSpaces))
|
|
||||||
copy(childSpaces, currentParams.childSpaces)
|
|
||||||
|
|
||||||
if len(child.Children) > 0 && !child.Data.ViewInfo.Collapsed {
|
|
||||||
childSpaces = append(childSpaces, isLast)
|
|
||||||
}
|
|
||||||
|
|
||||||
childParams = append(childParams, renderParams{
|
|
||||||
node: child,
|
|
||||||
spaces: currentParams.childSpaces,
|
|
||||||
childSpaces: childSpaces,
|
|
||||||
showCollapsed: showCollapsed,
|
|
||||||
isLast: isLast,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// keep the child nodes to visit later
|
|
||||||
paramsToVisit = append(childParams, paramsToVisit...)
|
|
||||||
|
|
||||||
// never process the root node
|
|
||||||
if currentParams.node == tree.Root {
|
|
||||||
currentRow--
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// process the current node
|
|
||||||
if currentRow >= startRow && currentRow <= stopRow {
|
|
||||||
params = append(params, currentParams)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// render the result
|
|
||||||
for idx := range params {
|
|
||||||
currentParams := params[idx]
|
|
||||||
|
|
||||||
if showAttributes {
|
|
||||||
result += currentParams.node.MetadataString() + " "
|
|
||||||
}
|
|
||||||
result += currentParams.node.renderTreeLine(currentParams.spaces, currentParams.isLast, currentParams.showCollapsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tree *FileTree) VisibleSize() int {
|
|
||||||
var size int
|
|
||||||
|
|
||||||
visitor := func(node *FileNode) error {
|
|
||||||
size++
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
visitEvaluator := func(node *FileNode) bool {
|
|
||||||
if node.Data.FileInfo.IsDir {
|
|
||||||
// we won't visit a collapsed dir, but we need to count it
|
|
||||||
if node.Data.ViewInfo.Collapsed {
|
|
||||||
size++
|
|
||||||
}
|
|
||||||
return !node.Data.ViewInfo.Collapsed && !node.Data.ViewInfo.Hidden
|
|
||||||
}
|
|
||||||
return !node.Data.ViewInfo.Hidden
|
|
||||||
}
|
|
||||||
err := tree.VisitDepthParentFirst(visitor, visitEvaluator)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Errorf("unable to determine visible tree size: %+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't include root
|
|
||||||
size--
|
|
||||||
|
|
||||||
return size
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the entire tree in an ASCII representation.
|
|
||||||
func (tree *FileTree) String(showAttributes bool) string {
|
|
||||||
return tree.renderStringTreeBetween(0, tree.Size, showAttributes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StringBetween returns a partial tree in an ASCII representation.
|
|
||||||
func (tree *FileTree) StringBetween(start, stop int, showAttributes bool) string {
|
|
||||||
return tree.renderStringTreeBetween(start, stop, showAttributes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy returns a copy of the given FileTree
|
|
||||||
func (tree *FileTree) Copy() *FileTree {
|
|
||||||
newTree := NewFileTree()
|
|
||||||
newTree.Size = tree.Size
|
|
||||||
newTree.FileSize = tree.FileSize
|
|
||||||
newTree.Root = tree.Root.Copy(newTree.Root)
|
|
||||||
|
|
||||||
// update the tree pointers
|
|
||||||
err := newTree.VisitDepthChildFirst(func(node *FileNode) error {
|
|
||||||
node.Tree = newTree
|
|
||||||
return nil
|
|
||||||
}, nil)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logrus.Errorf("unable to propagate tree on copy(): %+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return newTree
|
|
||||||
}
|
|
||||||
|
|
||||||
// Visitor is a function that processes, observes, or otherwise transforms the given node
|
|
||||||
type Visitor func(*FileNode) error
|
|
||||||
|
|
||||||
// VisitEvaluator is a function that indicates whether the given node should be visited by a Visitor.
|
|
||||||
type VisitEvaluator func(*FileNode) bool
|
|
||||||
|
|
||||||
// VisitDepthChildFirst iterates the given tree depth-first, evaluating the deepest depths first (visit on bubble up)
|
|
||||||
func (tree *FileTree) VisitDepthChildFirst(visitor Visitor, evaluator VisitEvaluator) error {
|
|
||||||
return tree.Root.VisitDepthChildFirst(visitor, evaluator)
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitDepthParentFirst iterates the given tree depth-first, evaluating the shallowest depths first (visit while sinking down)
|
|
||||||
func (tree *FileTree) VisitDepthParentFirst(visitor Visitor, evaluator VisitEvaluator) error {
|
|
||||||
return tree.Root.VisitDepthParentFirst(visitor, evaluator)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stack takes two trees and combines them together. This is done by "stacking" the given tree on top of the owning tree.
|
|
||||||
func (tree *FileTree) Stack(upper *FileTree) error {
|
|
||||||
graft := func(node *FileNode) 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.FileInfo)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot add node %s: %v", newNode.Path(), err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return upper.VisitDepthChildFirst(graft, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNode fetches a single node when given a slash-delimited string from root ('/') to the desired node (e.g. '/a/node/path')
|
|
||||||
func (tree *FileTree) GetNode(path string) (*FileNode, error) {
|
|
||||||
nodeNames := strings.Split(strings.Trim(path, "/"), "/")
|
|
||||||
node := tree.Root
|
|
||||||
for _, name := range nodeNames {
|
|
||||||
if name == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if node.Children[name] == nil {
|
|
||||||
return nil, fmt.Errorf("path does not exist: %s", path)
|
|
||||||
}
|
|
||||||
node = node.Children[name]
|
|
||||||
}
|
|
||||||
return node, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddPath adds a new node to the tree with the given payload
|
|
||||||
func (tree *FileTree) AddPath(path string, data FileInfo) (*FileNode, []*FileNode, error) {
|
|
||||||
nodeNames := strings.Split(strings.Trim(path, "/"), "/")
|
|
||||||
node := tree.Root
|
|
||||||
addedNodes := make([]*FileNode, 0)
|
|
||||||
for idx, name := range nodeNames {
|
|
||||||
if name == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// find or create node
|
|
||||||
if node.Children[name] != nil {
|
|
||||||
node = node.Children[name]
|
|
||||||
} else {
|
|
||||||
// don't add paths that should be deleted
|
|
||||||
if strings.HasPrefix(name, doubleWhiteoutPrefix) {
|
|
||||||
return nil, addedNodes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't attach the payload. The payload is destined for the
|
|
||||||
// Path's end node, not any intermediary node.
|
|
||||||
node = node.AddChild(name, FileInfo{})
|
|
||||||
addedNodes = append(addedNodes, node)
|
|
||||||
|
|
||||||
if node == nil {
|
|
||||||
// the child could not be added
|
|
||||||
return node, addedNodes, fmt.Errorf(fmt.Sprintf("could not add child node: '%s' (path:'%s')", name, path))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// attach payload to the last specified node
|
|
||||||
if idx == len(nodeNames)-1 {
|
|
||||||
node.Data.FileInfo = data
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return node, addedNodes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemovePath removes a node from the tree given its path.
|
|
||||||
func (tree *FileTree) RemovePath(path string) error {
|
|
||||||
node, err := tree.GetNode(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return node.Remove()
|
|
||||||
}
|
|
||||||
|
|
||||||
type compareMark struct {
|
|
||||||
lowerNode *FileNode
|
|
||||||
upperNode *FileNode
|
|
||||||
tentative DiffType
|
|
||||||
final DiffType
|
|
||||||
}
|
|
||||||
|
|
||||||
// CompareAndMark marks the FileNodes in the owning (lower) tree with DiffType annotations when compared to the given (upper) tree.
|
|
||||||
func (tree *FileTree) CompareAndMark(upper *FileTree) error {
|
|
||||||
// always compare relative to the original, unaltered tree.
|
|
||||||
originalTree := tree
|
|
||||||
|
|
||||||
modifications := make([]compareMark, 0)
|
|
||||||
|
|
||||||
graft := func(upperNode *FileNode) error {
|
|
||||||
if upperNode.IsWhiteout() {
|
|
||||||
err := tree.markRemoved(upperNode.Path())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot remove upperNode %s: %v", upperNode.Path(), err.Error())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// note: since we are not comparing against the original tree (copying the tree is expensive) we may mark the parent
|
|
||||||
// of an added node incorrectly as modified. This will be corrected later.
|
|
||||||
originalLowerNode, _ := originalTree.GetNode(upperNode.Path())
|
|
||||||
|
|
||||||
if originalLowerNode == nil {
|
|
||||||
_, newNodes, err := tree.AddPath(upperNode.Path(), upperNode.Data.FileInfo)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot add new upperNode %s: %v", upperNode.Path(), err.Error())
|
|
||||||
}
|
|
||||||
for idx := len(newNodes) - 1; idx >= 0; idx-- {
|
|
||||||
newNode := newNodes[idx]
|
|
||||||
modifications = append(modifications, compareMark{lowerNode: newNode, upperNode: upperNode, tentative: -1, final: Added})
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// the file exists in the lower layer
|
|
||||||
lowerNode, _ := tree.GetNode(upperNode.Path())
|
|
||||||
diffType := lowerNode.compare(upperNode)
|
|
||||||
modifications = append(modifications, compareMark{lowerNode: lowerNode, upperNode: upperNode, tentative: diffType, final: -1})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// we must visit from the leaves upwards to ensure that diff types can be derived from and assigned to children
|
|
||||||
err := upper.VisitDepthChildFirst(graft, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// take note of the comparison results on each note in the owning tree.
|
|
||||||
for _, pair := range modifications {
|
|
||||||
if pair.final > 0 {
|
|
||||||
err = pair.lowerNode.AssignDiffType(pair.final)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else if pair.lowerNode.Data.DiffType == Unmodified {
|
|
||||||
err = pair.lowerNode.deriveDiffType(pair.tentative)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// persist the upper's payload on the owning tree
|
|
||||||
pair.lowerNode.Data.FileInfo = *pair.upperNode.Data.FileInfo.Copy()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// markRemoved annotates the FileNode at the given path as Removed.
|
|
||||||
func (tree *FileTree) markRemoved(path string) error {
|
|
||||||
node, err := tree.GetNode(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return node.AssignDiffType(Removed)
|
|
||||||
}
|
|
||||||
|
|
||||||
// StackTreeRange combines an array of trees into a single tree
|
|
||||||
func StackTreeRange(trees []*FileTree, start, stop int) *FileTree {
|
|
||||||
|
|
||||||
tree := trees[0].Copy()
|
|
||||||
for idx := start; idx <= stop; idx++ {
|
|
||||||
err := tree.Stack(trees[idx])
|
|
||||||
if err != nil {
|
|
||||||
logrus.Errorf("could not stack tree range: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tree
|
|
||||||
}
|
|
@ -1,791 +0,0 @@
|
|||||||
package filetree
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func stringInSlice(a string, list []string) bool {
|
|
||||||
for _, b := range list {
|
|
||||||
if b == a {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func AssertDiffType(node *FileNode, expectedDiffType DiffType) error {
|
|
||||||
if node.Data.DiffType != expectedDiffType {
|
|
||||||
return fmt.Errorf("Expecting node at %s to have DiffType %v, but had %v", node.Path(), expectedDiffType, node.Data.DiffType)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStringCollapsed(t *testing.T) {
|
|
||||||
tree := NewFileTree()
|
|
||||||
tree.Root.AddChild("1 node!", FileInfo{})
|
|
||||||
two := tree.Root.AddChild("2 node!", FileInfo{})
|
|
||||||
subTwo := two.AddChild("2 child!", FileInfo{})
|
|
||||||
subTwo.AddChild("2 grandchild!", FileInfo{})
|
|
||||||
subTwo.Data.ViewInfo.Collapsed = true
|
|
||||||
three := tree.Root.AddChild("3 node!", FileInfo{})
|
|
||||||
subThree := three.AddChild("3 child!", FileInfo{})
|
|
||||||
three.AddChild("3 nested child 1!", FileInfo{})
|
|
||||||
threeGc1 := subThree.AddChild("3 grandchild 1!", FileInfo{})
|
|
||||||
threeGc1.AddChild("3 greatgrandchild 1!", FileInfo{})
|
|
||||||
subThree.AddChild("3 grandchild 2!", FileInfo{})
|
|
||||||
four := tree.Root.AddChild("4 node!", FileInfo{})
|
|
||||||
four.Data.ViewInfo.Collapsed = true
|
|
||||||
tree.Root.AddChild("5 node!", FileInfo{})
|
|
||||||
four.AddChild("6, one level down...", FileInfo{})
|
|
||||||
|
|
||||||
expected :=
|
|
||||||
`├── 1 node!
|
|
||||||
├── 2 node!
|
|
||||||
│ └─⊕ 2 child!
|
|
||||||
├── 3 node!
|
|
||||||
│ ├── 3 child!
|
|
||||||
│ │ ├── 3 grandchild 1!
|
|
||||||
│ │ │ └── 3 greatgrandchild 1!
|
|
||||||
│ │ └── 3 grandchild 2!
|
|
||||||
│ └── 3 nested child 1!
|
|
||||||
├─⊕ 4 node!
|
|
||||||
└── 5 node!
|
|
||||||
`
|
|
||||||
actual := tree.String(false)
|
|
||||||
|
|
||||||
if expected != actual {
|
|
||||||
t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestString(t *testing.T) {
|
|
||||||
tree := NewFileTree()
|
|
||||||
tree.Root.AddChild("1 node!", FileInfo{})
|
|
||||||
tree.Root.AddChild("2 node!", FileInfo{})
|
|
||||||
tree.Root.AddChild("3 node!", FileInfo{})
|
|
||||||
four := tree.Root.AddChild("4 node!", FileInfo{})
|
|
||||||
tree.Root.AddChild("5 node!", FileInfo{})
|
|
||||||
four.AddChild("6, one level down...", FileInfo{})
|
|
||||||
|
|
||||||
expected :=
|
|
||||||
`├── 1 node!
|
|
||||||
├── 2 node!
|
|
||||||
├── 3 node!
|
|
||||||
├── 4 node!
|
|
||||||
│ └── 6, one level down...
|
|
||||||
└── 5 node!
|
|
||||||
`
|
|
||||||
actual := tree.String(false)
|
|
||||||
|
|
||||||
if expected != actual {
|
|
||||||
t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStringBetween(t *testing.T) {
|
|
||||||
tree := NewFileTree()
|
|
||||||
_, _, err := tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/etc/nginx/public", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/var/run/systemd", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/var/run/bashful", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/tmp", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/tmp/nonsense", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected :=
|
|
||||||
`│ └── public
|
|
||||||
├── tmp
|
|
||||||
│ └── nonsense
|
|
||||||
`
|
|
||||||
actual := tree.StringBetween(3, 5, false)
|
|
||||||
|
|
||||||
if expected != actual {
|
|
||||||
t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddPath(t *testing.T) {
|
|
||||||
tree := NewFileTree()
|
|
||||||
_, _, err := tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/etc/nginx/public", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/var/run/systemd", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/var/run/bashful", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/tmp", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/tmp/nonsense", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected :=
|
|
||||||
`├── etc
|
|
||||||
│ └── nginx
|
|
||||||
│ ├── nginx.conf
|
|
||||||
│ └── public
|
|
||||||
├── tmp
|
|
||||||
│ └── nonsense
|
|
||||||
└── var
|
|
||||||
└── run
|
|
||||||
├── bashful
|
|
||||||
└── systemd
|
|
||||||
`
|
|
||||||
actual := tree.String(false)
|
|
||||||
|
|
||||||
if expected != actual {
|
|
||||||
t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddWhiteoutPath(t *testing.T) {
|
|
||||||
tree := NewFileTree()
|
|
||||||
node, _, err := tree.AddPath("usr/local/lib/python3.7/site-packages/pip/.wh..wh..opq", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("expected no error but got: %v", err)
|
|
||||||
}
|
|
||||||
if node != nil {
|
|
||||||
t.Errorf("expected node to be nil, but got: %v", node)
|
|
||||||
}
|
|
||||||
expected :=
|
|
||||||
`└── usr
|
|
||||||
└── local
|
|
||||||
└── lib
|
|
||||||
└── python3.7
|
|
||||||
└── site-packages
|
|
||||||
└── pip
|
|
||||||
`
|
|
||||||
actual := tree.String(false)
|
|
||||||
|
|
||||||
if expected != actual {
|
|
||||||
t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemovePath(t *testing.T) {
|
|
||||||
tree := NewFileTree()
|
|
||||||
_, _, err := tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/etc/nginx/public", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/var/run/systemd", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/var/run/bashful", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/tmp", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/tmp/nonsense", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tree.RemovePath("/var/run/bashful")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
err = tree.RemovePath("/tmp")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected :=
|
|
||||||
`├── etc
|
|
||||||
│ └── nginx
|
|
||||||
│ ├── nginx.conf
|
|
||||||
│ └── public
|
|
||||||
└── var
|
|
||||||
└── run
|
|
||||||
└── systemd
|
|
||||||
`
|
|
||||||
actual := tree.String(false)
|
|
||||||
|
|
||||||
if expected != actual {
|
|
||||||
t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStack(t *testing.T) {
|
|
||||||
payloadKey := "/var/run/systemd"
|
|
||||||
payloadValue := FileInfo{
|
|
||||||
Path: "yup",
|
|
||||||
}
|
|
||||||
|
|
||||||
tree1 := NewFileTree()
|
|
||||||
|
|
||||||
_, _, err := tree1.AddPath("/etc/nginx/public", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree1.AddPath(payloadKey, FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree1.AddPath("/var/run/bashful", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree1.AddPath("/tmp", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree1.AddPath("/tmp/nonsense", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tree2 := NewFileTree()
|
|
||||||
// add new files
|
|
||||||
_, _, err = tree2.AddPath("/etc/nginx/nginx.conf", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
// modify current files
|
|
||||||
_, _, err = tree2.AddPath(payloadKey, payloadValue)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
// whiteout the following files
|
|
||||||
_, _, err = tree2.AddPath("/var/run/.wh.bashful", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree2.AddPath("/.wh.tmp", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
// ignore opaque whiteout files entirely
|
|
||||||
node, _, err := tree2.AddPath("/.wh..wh..opq", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("expected no error on whiteout file add, but got %v", err)
|
|
||||||
}
|
|
||||||
if node != nil {
|
|
||||||
t.Errorf("expected no node on whiteout file add, but got %v", node)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tree1.Stack(tree2)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Could not stack refTrees: %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 == nil || node.Data.FileInfo.Path != payloadValue.Path {
|
|
||||||
t.Errorf("Expected '%s' value to be %+v but got %+v", payloadKey, payloadValue, node.Data.FileInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := tree1.String(false)
|
|
||||||
|
|
||||||
if expected != actual {
|
|
||||||
t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCopy(t *testing.T) {
|
|
||||||
tree := NewFileTree()
|
|
||||||
_, _, err := tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/etc/nginx/public", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/var/run/systemd", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/var/run/bashful", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/tmp", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/tmp/nonsense", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tree.RemovePath("/var/run/bashful")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
err = tree.RemovePath("/tmp")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected :=
|
|
||||||
`├── etc
|
|
||||||
│ └── nginx
|
|
||||||
│ ├── nginx.conf
|
|
||||||
│ └── public
|
|
||||||
└── var
|
|
||||||
└── run
|
|
||||||
└── systemd
|
|
||||||
`
|
|
||||||
|
|
||||||
NewFileTree := tree.Copy()
|
|
||||||
actual := NewFileTree.String(false)
|
|
||||||
|
|
||||||
if expected != actual {
|
|
||||||
t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompareWithNoChanges(t *testing.T) {
|
|
||||||
lowerTree := NewFileTree()
|
|
||||||
upperTree := NewFileTree()
|
|
||||||
paths := [...]string{"/etc", "/etc/sudoers", "/etc/hosts", "/usr/bin", "/usr/bin/bash", "/usr"}
|
|
||||||
|
|
||||||
for _, value := range paths {
|
|
||||||
fakeData := FileInfo{
|
|
||||||
Path: value,
|
|
||||||
TypeFlag: 1,
|
|
||||||
hash: 123,
|
|
||||||
}
|
|
||||||
_, _, err := lowerTree.AddPath(value, fakeData)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = upperTree.AddPath(value, fakeData)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err := lowerTree.CompareAndMark(upperTree)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
asserter := func(n *FileNode) error {
|
|
||||||
if n.Path() == "/" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if (n.Data.DiffType) != Unmodified {
|
|
||||||
t.Errorf("Expecting node at %s to have DiffType unchanged, but had %v", n.Path(), n.Data.DiffType)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err = lowerTree.VisitDepthChildFirst(asserter, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompareWithAdds(t *testing.T) {
|
|
||||||
lowerTree := NewFileTree()
|
|
||||||
upperTree := NewFileTree()
|
|
||||||
lowerPaths := [...]string{"/etc", "/etc/sudoers", "/usr", "/etc/hosts", "/usr/bin"}
|
|
||||||
upperPaths := [...]string{"/etc", "/etc/sudoers", "/usr", "/etc/hosts", "/usr/bin", "/usr/bin/bash", "/a/new/path"}
|
|
||||||
|
|
||||||
for _, value := range lowerPaths {
|
|
||||||
_, _, err := lowerTree.AddPath(value, FileInfo{
|
|
||||||
Path: value,
|
|
||||||
TypeFlag: 1,
|
|
||||||
hash: 123,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, value := range upperPaths {
|
|
||||||
_, _, err := upperTree.AddPath(value, FileInfo{
|
|
||||||
Path: value,
|
|
||||||
TypeFlag: 1,
|
|
||||||
hash: 123,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
failedAssertions := []error{}
|
|
||||||
err := lowerTree.CompareAndMark(upperTree)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected tree compare to have no errors, got: %v", err)
|
|
||||||
}
|
|
||||||
asserter := func(n *FileNode) error {
|
|
||||||
|
|
||||||
p := n.Path()
|
|
||||||
if p == "/" {
|
|
||||||
return nil
|
|
||||||
} else if stringInSlice(p, []string{"/usr/bin/bash", "/a", "/a/new", "/a/new/path"}) {
|
|
||||||
if err := AssertDiffType(n, Added); err != nil {
|
|
||||||
failedAssertions = append(failedAssertions, err)
|
|
||||||
}
|
|
||||||
} else if stringInSlice(p, []string{"/usr/bin", "/usr"}) {
|
|
||||||
if err := AssertDiffType(n, Modified); err != nil {
|
|
||||||
failedAssertions = append(failedAssertions, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := AssertDiffType(n, Unmodified); err != nil {
|
|
||||||
failedAssertions = append(failedAssertions, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err = lowerTree.VisitDepthChildFirst(asserter, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected no errors when visiting nodes, got: %+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(failedAssertions) > 0 {
|
|
||||||
str := "\n"
|
|
||||||
for _, value := range failedAssertions {
|
|
||||||
str += fmt.Sprintf(" - %s\n", value.Error())
|
|
||||||
}
|
|
||||||
t.Errorf("Expected no errors when evaluating nodes, got: %s", str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompareWithChanges(t *testing.T) {
|
|
||||||
lowerTree := NewFileTree()
|
|
||||||
upperTree := NewFileTree()
|
|
||||||
changedPaths := []string{"/etc", "/usr", "/etc/hosts", "/etc/sudoers", "/usr/bin"}
|
|
||||||
|
|
||||||
for _, value := range changedPaths {
|
|
||||||
_, _, err := lowerTree.AddPath(value, FileInfo{
|
|
||||||
Path: value,
|
|
||||||
TypeFlag: 1,
|
|
||||||
hash: 123,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = upperTree.AddPath(value, FileInfo{
|
|
||||||
Path: value,
|
|
||||||
TypeFlag: 1,
|
|
||||||
hash: 456,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chmodPath := "/etc/non-data-change"
|
|
||||||
|
|
||||||
_, _, err := lowerTree.AddPath(chmodPath, FileInfo{
|
|
||||||
Path: chmodPath,
|
|
||||||
TypeFlag: 1,
|
|
||||||
hash: 123,
|
|
||||||
Mode: 0,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _, err = upperTree.AddPath(chmodPath, FileInfo{
|
|
||||||
Path: chmodPath,
|
|
||||||
TypeFlag: 1,
|
|
||||||
hash: 123,
|
|
||||||
Mode: 1,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
changedPaths = append(changedPaths, chmodPath)
|
|
||||||
|
|
||||||
chownPath := "/etc/non-data-change-2"
|
|
||||||
|
|
||||||
_, _, err = lowerTree.AddPath(chmodPath, FileInfo{
|
|
||||||
Path: chownPath,
|
|
||||||
TypeFlag: 1,
|
|
||||||
hash: 123,
|
|
||||||
Mode: 1,
|
|
||||||
Gid: 0,
|
|
||||||
Uid: 0,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _, err = upperTree.AddPath(chmodPath, FileInfo{
|
|
||||||
Path: chownPath,
|
|
||||||
TypeFlag: 1,
|
|
||||||
hash: 123,
|
|
||||||
Mode: 1,
|
|
||||||
Gid: 12,
|
|
||||||
Uid: 12,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
changedPaths = append(changedPaths, chownPath)
|
|
||||||
|
|
||||||
err = lowerTree.CompareAndMark(upperTree)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("unable to compare and mark: %+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
failedAssertions := []error{}
|
|
||||||
asserter := func(n *FileNode) error {
|
|
||||||
p := n.Path()
|
|
||||||
if p == "/" {
|
|
||||||
return nil
|
|
||||||
} else if stringInSlice(p, changedPaths) {
|
|
||||||
if err := AssertDiffType(n, Modified); err != nil {
|
|
||||||
failedAssertions = append(failedAssertions, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := AssertDiffType(n, Unmodified); err != nil {
|
|
||||||
failedAssertions = append(failedAssertions, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err = lowerTree.VisitDepthChildFirst(asserter, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected no errors when visiting nodes, got: %+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(failedAssertions) > 0 {
|
|
||||||
str := "\n"
|
|
||||||
for _, value := range failedAssertions {
|
|
||||||
str += fmt.Sprintf(" - %s\n", value.Error())
|
|
||||||
}
|
|
||||||
t.Errorf("Expected no errors when evaluating nodes, got: %s", str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompareWithRemoves(t *testing.T) {
|
|
||||||
lowerTree := NewFileTree()
|
|
||||||
upperTree := NewFileTree()
|
|
||||||
lowerPaths := [...]string{"/etc", "/usr", "/etc/hosts", "/etc/sudoers", "/usr/bin", "/root", "/root/example", "/root/example/some1", "/root/example/some2"}
|
|
||||||
upperPaths := [...]string{"/.wh.etc", "/usr", "/usr/.wh.bin", "/root/.wh.example"}
|
|
||||||
|
|
||||||
for _, value := range lowerPaths {
|
|
||||||
fakeData := FileInfo{
|
|
||||||
Path: value,
|
|
||||||
TypeFlag: 1,
|
|
||||||
hash: 123,
|
|
||||||
}
|
|
||||||
_, _, err := lowerTree.AddPath(value, fakeData)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, value := range upperPaths {
|
|
||||||
fakeData := FileInfo{
|
|
||||||
Path: value,
|
|
||||||
TypeFlag: 1,
|
|
||||||
hash: 123,
|
|
||||||
}
|
|
||||||
_, _, err := upperTree.AddPath(value, fakeData)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := lowerTree.CompareAndMark(upperTree)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
failedAssertions := []error{}
|
|
||||||
asserter := func(n *FileNode) error {
|
|
||||||
p := n.Path()
|
|
||||||
if p == "/" {
|
|
||||||
return nil
|
|
||||||
} else if stringInSlice(p, []string{"/etc", "/usr/bin", "/etc/hosts", "/etc/sudoers", "/root/example/some1", "/root/example/some2", "/root/example"}) {
|
|
||||||
if err := AssertDiffType(n, Removed); err != nil {
|
|
||||||
failedAssertions = append(failedAssertions, err)
|
|
||||||
}
|
|
||||||
} else if stringInSlice(p, []string{"/usr", "/root"}) {
|
|
||||||
if err := AssertDiffType(n, Modified); err != nil {
|
|
||||||
failedAssertions = append(failedAssertions, err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := AssertDiffType(n, Unmodified); err != nil {
|
|
||||||
failedAssertions = append(failedAssertions, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err = lowerTree.VisitDepthChildFirst(asserter, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected no errors when visiting nodes, got: %+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(failedAssertions) > 0 {
|
|
||||||
str := "\n"
|
|
||||||
for _, value := range failedAssertions {
|
|
||||||
str += fmt.Sprintf(" - %s\n", value.Error())
|
|
||||||
}
|
|
||||||
t.Errorf("Expected no errors when evaluating nodes, got: %s", str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStackRange(t *testing.T) {
|
|
||||||
tree := NewFileTree()
|
|
||||||
_, _, err := tree.AddPath("/etc/nginx/nginx.conf", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/etc/nginx/public", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/var/run/systemd", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/var/run/bashful", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/tmp", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
_, _, err = tree.AddPath("/tmp/nonsense", FileInfo{})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tree.RemovePath("/var/run/bashful")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
err = tree.RemovePath("/tmp")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
lowerTree := NewFileTree()
|
|
||||||
upperTree := NewFileTree()
|
|
||||||
lowerPaths := [...]string{"/etc", "/usr", "/etc/hosts", "/etc/sudoers", "/usr/bin"}
|
|
||||||
upperPaths := [...]string{"/etc", "/usr", "/etc/hosts", "/etc/sudoers", "/usr/bin"}
|
|
||||||
|
|
||||||
for _, value := range lowerPaths {
|
|
||||||
fakeData := FileInfo{
|
|
||||||
Path: value,
|
|
||||||
TypeFlag: 1,
|
|
||||||
hash: 123,
|
|
||||||
}
|
|
||||||
_, _, err = lowerTree.AddPath(value, fakeData)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, value := range upperPaths {
|
|
||||||
fakeData := FileInfo{
|
|
||||||
Path: value,
|
|
||||||
TypeFlag: 1,
|
|
||||||
hash: 456,
|
|
||||||
}
|
|
||||||
_, _, err = upperTree.AddPath(value, fakeData)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
trees := []*FileTree{lowerTree, upperTree, tree}
|
|
||||||
StackTreeRange(trees, 0, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRemoveOnIterate(t *testing.T) {
|
|
||||||
|
|
||||||
tree := NewFileTree()
|
|
||||||
paths := [...]string{"/etc", "/usr", "/etc/hosts", "/etc/sudoers", "/usr/bin", "/usr/something"}
|
|
||||||
|
|
||||||
for _, value := range paths {
|
|
||||||
fakeData := FileInfo{
|
|
||||||
Path: value,
|
|
||||||
TypeFlag: 1,
|
|
||||||
hash: 123,
|
|
||||||
}
|
|
||||||
node, _, err := tree.AddPath(value, fakeData)
|
|
||||||
if err == nil && stringInSlice(node.Path(), []string{"/etc"}) {
|
|
||||||
node.Data.ViewInfo.Hidden = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := tree.VisitDepthChildFirst(func(node *FileNode) error {
|
|
||||||
if node.Data.ViewInfo.Hidden {
|
|
||||||
err := tree.RemovePath(node.Path())
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("could not setup test: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected :=
|
|
||||||
`└── usr
|
|
||||||
├── bin
|
|
||||||
└── something
|
|
||||||
`
|
|
||||||
actual := tree.String(false)
|
|
||||||
if expected != actual {
|
|
||||||
t.Errorf("Expected tree string:\n--->%s<---\nGot:\n--->%s<---", expected, actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
package filetree
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FileTree represents a set of files, directories, and their relations.
|
|
||||||
type FileTree struct {
|
|
||||||
Root *FileNode
|
|
||||||
Size int
|
|
||||||
FileSize uint64
|
|
||||||
Name string
|
|
||||||
Id uuid.UUID
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileNode represents a single file, its relation to files beneath it, the tree it exists in, and the metadata of the given file.
|
|
||||||
type FileNode struct {
|
|
||||||
Tree *FileTree
|
|
||||||
Parent *FileNode
|
|
||||||
Name string
|
|
||||||
Data NodeData
|
|
||||||
Children map[string]*FileNode
|
|
||||||
path string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NodeData is the payload for a FileNode
|
|
||||||
type NodeData struct {
|
|
||||||
ViewInfo ViewInfo
|
|
||||||
FileInfo FileInfo
|
|
||||||
DiffType DiffType
|
|
||||||
}
|
|
||||||
|
|
||||||
// ViewInfo contains UI specific detail for a specific FileNode
|
|
||||||
type ViewInfo struct {
|
|
||||||
Collapsed bool
|
|
||||||
Hidden bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// FileInfo contains tar metadata for a specific FileNode
|
|
||||||
type FileInfo struct {
|
|
||||||
Path string
|
|
||||||
TypeFlag byte
|
|
||||||
Linkname string
|
|
||||||
hash uint64
|
|
||||||
Size int64
|
|
||||||
Mode os.FileMode
|
|
||||||
Uid int
|
|
||||||
Gid int
|
|
||||||
IsDir bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiffType defines the comparison result between two FileNodes
|
|
||||||
type DiffType int
|
|
||||||
|
|
||||||
// EfficiencyData represents the storage and reference statistics for a given file tree path.
|
|
||||||
type EfficiencyData struct {
|
|
||||||
Path string
|
|
||||||
Nodes []*FileNode
|
|
||||||
CumulativeSize int64
|
|
||||||
minDiscoveredSize int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// EfficiencySlice represents an ordered set of EfficiencyData data structures.
|
|
||||||
type EfficiencySlice []*EfficiencyData
|
|
1
go.mod
1
go.mod
@ -46,6 +46,7 @@ require (
|
|||||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297
|
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297
|
||||||
golang.org/x/sys v0.0.0-20190907184412-d223b2b6db03 // indirect
|
golang.org/x/sys v0.0.0-20190907184412-d223b2b6db03 // indirect
|
||||||
golang.org/x/text v0.3.2 // indirect
|
golang.org/x/text v0.3.2 // indirect
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8
|
||||||
gotest.tools v2.2.0+incompatible // indirect
|
gotest.tools v2.2.0+incompatible // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
2
go.sum
2
go.sum
@ -99,6 +99,7 @@ github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
|
|||||||
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
|
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0=
|
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0=
|
||||||
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
|
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4=
|
||||||
@ -405,6 +406,7 @@ golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3
|
|||||||
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd h1:7E3PabyysDSEjnaANKBgums/hyvMI/HoHQ50qZEzTrg=
|
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd h1:7E3PabyysDSEjnaANKBgums/hyvMI/HoHQ50qZEzTrg=
|
||||||
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
|
@ -1,293 +0,0 @@
|
|||||||
package image
|
|
||||||
|
|
||||||
import (
|
|
||||||
"archive/tar"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli/connhelper"
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/wagoodman/dive/filetree"
|
|
||||||
"github.com/wagoodman/dive/utils"
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
)
|
|
||||||
|
|
||||||
var dockerVersion string
|
|
||||||
|
|
||||||
func newDockerImageAnalyzer(imageId string) Analyzer {
|
|
||||||
return &dockerImageAnalyzer{
|
|
||||||
// store discovered json files in a map so we can read the image in one pass
|
|
||||||
jsonFiles: make(map[string][]byte),
|
|
||||||
layerMap: make(map[string]*filetree.FileTree),
|
|
||||||
id: imageId,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDockerImageManifest(manifestBytes []byte) dockerImageManifest {
|
|
||||||
var manifest []dockerImageManifest
|
|
||||||
err := json.Unmarshal(manifestBytes, &manifest)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Panic(err)
|
|
||||||
}
|
|
||||||
return manifest[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDockerImageConfig(configBytes []byte) dockerImageConfig {
|
|
||||||
var imageConfig dockerImageConfig
|
|
||||||
err := json.Unmarshal(configBytes, &imageConfig)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
layerIdx := 0
|
|
||||||
for idx := range imageConfig.History {
|
|
||||||
if imageConfig.History[idx].EmptyLayer {
|
|
||||||
imageConfig.History[idx].ID = "<missing>"
|
|
||||||
} else {
|
|
||||||
imageConfig.History[idx].ID = imageConfig.RootFs.DiffIds[layerIdx]
|
|
||||||
layerIdx++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return imageConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func (image *dockerImageAnalyzer) Fetch() (io.ReadCloser, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// pull the image if it does not exist
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
host := os.Getenv("DOCKER_HOST")
|
|
||||||
var clientOpts []func(*client.Client) error
|
|
||||||
|
|
||||||
switch strings.Split(host, ":")[0] {
|
|
||||||
case "ssh":
|
|
||||||
helper, err := connhelper.GetConnectionHelper(host)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("docker host", err)
|
|
||||||
}
|
|
||||||
clientOpts = append(clientOpts, func(c *client.Client) error {
|
|
||||||
httpClient := &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DialContext: helper.Dialer,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return client.WithHTTPClient(httpClient)(c)
|
|
||||||
})
|
|
||||||
clientOpts = append(clientOpts, client.WithHost(helper.Host))
|
|
||||||
clientOpts = append(clientOpts, client.WithDialContext(helper.Dialer))
|
|
||||||
|
|
||||||
default:
|
|
||||||
|
|
||||||
if os.Getenv("DOCKER_TLS_VERIFY") != "" && os.Getenv("DOCKER_CERT_PATH") == "" {
|
|
||||||
os.Setenv("DOCKER_CERT_PATH", "~/.docker")
|
|
||||||
}
|
|
||||||
|
|
||||||
clientOpts = append(clientOpts, client.FromEnv)
|
|
||||||
}
|
|
||||||
|
|
||||||
clientOpts = append(clientOpts, client.WithVersion(dockerVersion))
|
|
||||||
image.client, err = client.NewClientWithOpts(clientOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, _, err = image.client.ImageInspectWithRaw(ctx, image.id)
|
|
||||||
if err != nil {
|
|
||||||
|
|
||||||
if !utils.IsDockerClientAvailable() {
|
|
||||||
return nil, fmt.Errorf("cannot find docker client executable")
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't use the API, the CLI has more informative output
|
|
||||||
fmt.Println("Image not available locally. Trying to pull '" + image.id + "'...")
|
|
||||||
err = utils.RunDockerCmd("pull", image.id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readCloser, err := image.client.ImageSave(ctx, []string{image.id})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return readCloser, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (image *dockerImageAnalyzer) Parse(tarFile io.ReadCloser) error {
|
|
||||||
tarReader := tar.NewReader(tarFile)
|
|
||||||
|
|
||||||
var currentLayer uint
|
|
||||||
for {
|
|
||||||
header, err := tarReader.Next()
|
|
||||||
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
utils.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
name := header.Name
|
|
||||||
|
|
||||||
// some layer tars can be relative layer symlinks to other layer tars
|
|
||||||
if header.Typeflag == tar.TypeSymlink || header.Typeflag == tar.TypeReg {
|
|
||||||
|
|
||||||
if strings.HasSuffix(name, "layer.tar") {
|
|
||||||
currentLayer++
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
layerReader := tar.NewReader(tarReader)
|
|
||||||
err := image.processLayerTar(name, currentLayer, layerReader)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else if strings.HasSuffix(name, ".json") {
|
|
||||||
fileBuffer, err := ioutil.ReadAll(tarReader)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
image.jsonFiles[name] = fileBuffer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (image *dockerImageAnalyzer) Analyze() (*AnalysisResult, error) {
|
|
||||||
image.trees = make([]*filetree.FileTree, 0)
|
|
||||||
|
|
||||||
manifest := newDockerImageManifest(image.jsonFiles["manifest.json"])
|
|
||||||
config := newDockerImageConfig(image.jsonFiles[manifest.ConfigPath])
|
|
||||||
|
|
||||||
// build the content tree
|
|
||||||
for _, treeName := range manifest.LayerTarPaths {
|
|
||||||
image.trees = append(image.trees, image.layerMap[treeName])
|
|
||||||
}
|
|
||||||
|
|
||||||
// build the layers array
|
|
||||||
image.layers = make([]*dockerLayer, len(image.trees))
|
|
||||||
|
|
||||||
// note that the image config stores images in reverse chronological order, so iterate backwards through layers
|
|
||||||
// as you iterate chronologically through history (ignoring history items that have no layer contents)
|
|
||||||
// Note: history is not required metadata in a docker image!
|
|
||||||
tarPathIdx := 0
|
|
||||||
histIdx := 0
|
|
||||||
for layerIdx := len(image.trees) - 1; layerIdx >= 0; layerIdx-- {
|
|
||||||
|
|
||||||
tree := image.trees[(len(image.trees)-1)-layerIdx]
|
|
||||||
|
|
||||||
// ignore empty layers, we are only observing layers with content
|
|
||||||
historyObj := dockerImageHistoryEntry{
|
|
||||||
CreatedBy: "(missing)",
|
|
||||||
}
|
|
||||||
for nextHistIdx := histIdx; nextHistIdx < len(config.History); nextHistIdx++ {
|
|
||||||
if !config.History[nextHistIdx].EmptyLayer {
|
|
||||||
histIdx = nextHistIdx
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if histIdx < len(config.History) && !config.History[histIdx].EmptyLayer {
|
|
||||||
historyObj = config.History[histIdx]
|
|
||||||
histIdx++
|
|
||||||
}
|
|
||||||
|
|
||||||
image.layers[layerIdx] = &dockerLayer{
|
|
||||||
history: historyObj,
|
|
||||||
index: tarPathIdx,
|
|
||||||
tree: image.trees[layerIdx],
|
|
||||||
tarPath: manifest.LayerTarPaths[tarPathIdx],
|
|
||||||
}
|
|
||||||
image.layers[layerIdx].history.Size = tree.FileSize
|
|
||||||
|
|
||||||
tarPathIdx++
|
|
||||||
}
|
|
||||||
|
|
||||||
efficiency, inefficiencies := filetree.Efficiency(image.trees)
|
|
||||||
|
|
||||||
var sizeBytes, userSizeBytes uint64
|
|
||||||
layers := make([]Layer, len(image.layers))
|
|
||||||
for i, v := range image.layers {
|
|
||||||
layers[i] = v
|
|
||||||
sizeBytes += v.Size()
|
|
||||||
if i != 0 {
|
|
||||||
userSizeBytes += v.Size()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var wastedBytes uint64
|
|
||||||
for idx := 0; idx < len(inefficiencies); idx++ {
|
|
||||||
fileData := inefficiencies[len(inefficiencies)-1-idx]
|
|
||||||
wastedBytes += uint64(fileData.CumulativeSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &AnalysisResult{
|
|
||||||
Layers: layers,
|
|
||||||
RefTrees: image.trees,
|
|
||||||
Efficiency: efficiency,
|
|
||||||
UserSizeByes: userSizeBytes,
|
|
||||||
SizeBytes: sizeBytes,
|
|
||||||
WastedBytes: wastedBytes,
|
|
||||||
WastedUserPercent: float64(wastedBytes) / float64(userSizeBytes),
|
|
||||||
Inefficiencies: inefficiencies,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (image *dockerImageAnalyzer) processLayerTar(name string, layerIdx uint, reader *tar.Reader) error {
|
|
||||||
tree := filetree.NewFileTree()
|
|
||||||
tree.Name = name
|
|
||||||
|
|
||||||
fileInfos, err := image.getFileList(reader)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, element := range fileInfos {
|
|
||||||
tree.FileSize += uint64(element.Size)
|
|
||||||
|
|
||||||
_, _, err := tree.AddPath(element.Path, element)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
image.layerMap[tree.Name] = tree
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (image *dockerImageAnalyzer) getFileList(tarReader *tar.Reader) ([]filetree.FileInfo, error) {
|
|
||||||
var files []filetree.FileInfo
|
|
||||||
|
|
||||||
for {
|
|
||||||
header, err := tarReader.Next()
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
} else if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
utils.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
name := header.Name
|
|
||||||
|
|
||||||
switch header.Typeflag {
|
|
||||||
case tar.TypeXGlobalHeader:
|
|
||||||
return nil, fmt.Errorf("unexptected tar file: (XGlobalHeader): type=%v name=%s", header.Typeflag, name)
|
|
||||||
case tar.TypeXHeader:
|
|
||||||
return nil, fmt.Errorf("unexptected tar file (XHeader): type=%v name=%s", header.Typeflag, name)
|
|
||||||
default:
|
|
||||||
files = append(files, filetree.NewFileInfo(tarReader, header, name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return files, nil
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
package image
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
|
||||||
"github.com/wagoodman/dive/filetree"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// LayerFormat = "%-15s %7s %s"
|
|
||||||
LayerFormat = "%7s %s"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ShortId returns the truncated id of the current layer.
|
|
||||||
func (layer *dockerLayer) TarId() string {
|
|
||||||
return strings.TrimSuffix(layer.tarPath, "/layer.tar")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShortId returns the truncated id of the current layer.
|
|
||||||
func (layer *dockerLayer) Id() string {
|
|
||||||
return layer.history.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// index returns the relative position of the layer within the image.
|
|
||||||
func (layer *dockerLayer) Index() int {
|
|
||||||
return layer.index
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size returns the number of bytes that this image is.
|
|
||||||
func (layer *dockerLayer) Size() uint64 {
|
|
||||||
return layer.history.Size
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tree returns the file tree representing the current layer.
|
|
||||||
func (layer *dockerLayer) Tree() *filetree.FileTree {
|
|
||||||
return layer.tree
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShortId returns the truncated id of the current layer.
|
|
||||||
func (layer *dockerLayer) Command() string {
|
|
||||||
return strings.TrimPrefix(layer.history.CreatedBy, "/bin/sh -c ")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShortId returns the truncated id of the current layer.
|
|
||||||
func (layer *dockerLayer) ShortId() string {
|
|
||||||
rangeBound := 15
|
|
||||||
id := layer.Id()
|
|
||||||
if length := len(id); length < 15 {
|
|
||||||
rangeBound = length
|
|
||||||
}
|
|
||||||
id = id[0:rangeBound]
|
|
||||||
|
|
||||||
// show the tagged image as the last layer
|
|
||||||
// if len(layer.History.Tags) > 0 {
|
|
||||||
// id = "[" + strings.Join(layer.History.Tags, ",") + "]"
|
|
||||||
// }
|
|
||||||
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
// String represents a layer in a columnar format.
|
|
||||||
func (layer *dockerLayer) String() string {
|
|
||||||
|
|
||||||
if layer.index == 0 {
|
|
||||||
return fmt.Sprintf(LayerFormat,
|
|
||||||
// layer.ShortId(),
|
|
||||||
// fmt.Sprintf("%d",layer.Index()),
|
|
||||||
humanize.Bytes(layer.Size()),
|
|
||||||
"FROM "+layer.ShortId())
|
|
||||||
}
|
|
||||||
return fmt.Sprintf(LayerFormat,
|
|
||||||
// layer.ShortId(),
|
|
||||||
// fmt.Sprintf("%d",layer.Index()),
|
|
||||||
humanize.Bytes(layer.Size()),
|
|
||||||
layer.Command())
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
package image
|
|
||||||
|
|
||||||
type AnalyzerFactory func(string) Analyzer
|
|
||||||
|
|
||||||
func GetAnalyzer(imageID string) Analyzer {
|
|
||||||
// todo: add ability to have multiple image formats... for the meantime only use docker
|
|
||||||
var factory AnalyzerFactory = newDockerImageAnalyzer
|
|
||||||
|
|
||||||
return factory(imageID)
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package image
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLoadDockerImageTar(tarPath string) (*AnalysisResult, error) {
|
|
||||||
f, err := os.Open(tarPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
analyzer := newDockerImageAnalyzer("dive-test:latest")
|
|
||||||
err = analyzer.Parse(f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return analyzer.Analyze()
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
package image
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/docker/docker/client"
|
|
||||||
"github.com/wagoodman/dive/filetree"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Parser interface {
|
|
||||||
}
|
|
||||||
|
|
||||||
type Analyzer interface {
|
|
||||||
Fetch() (io.ReadCloser, error)
|
|
||||||
Parse(io.ReadCloser) error
|
|
||||||
Analyze() (*AnalysisResult, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Layer interface {
|
|
||||||
Id() string
|
|
||||||
ShortId() string
|
|
||||||
Index() int
|
|
||||||
Command() string
|
|
||||||
Size() uint64
|
|
||||||
Tree() *filetree.FileTree
|
|
||||||
String() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type AnalysisResult struct {
|
|
||||||
Layers []Layer
|
|
||||||
RefTrees []*filetree.FileTree
|
|
||||||
Efficiency float64
|
|
||||||
SizeBytes uint64
|
|
||||||
UserSizeByes uint64 // this is all bytes except for the base image
|
|
||||||
WastedUserPercent float64 // = wasted-bytes/user-size-bytes
|
|
||||||
WastedBytes uint64
|
|
||||||
Inefficiencies filetree.EfficiencySlice
|
|
||||||
}
|
|
||||||
|
|
||||||
type dockerImageAnalyzer struct {
|
|
||||||
id string
|
|
||||||
client *client.Client
|
|
||||||
jsonFiles map[string][]byte
|
|
||||||
trees []*filetree.FileTree
|
|
||||||
layerMap map[string]*filetree.FileTree
|
|
||||||
layers []*dockerLayer
|
|
||||||
}
|
|
||||||
|
|
||||||
type dockerImageHistoryEntry struct {
|
|
||||||
ID string
|
|
||||||
Size uint64
|
|
||||||
Created string `json:"created"`
|
|
||||||
Author string `json:"author"`
|
|
||||||
CreatedBy string `json:"created_by"`
|
|
||||||
EmptyLayer bool `json:"empty_layer"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type dockerImageManifest struct {
|
|
||||||
ConfigPath string `json:"Config"`
|
|
||||||
RepoTags []string `json:"RepoTags"`
|
|
||||||
LayerTarPaths []string `json:"Layers"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type dockerImageConfig struct {
|
|
||||||
History []dockerImageHistoryEntry `json:"history"`
|
|
||||||
RootFs dockerRootFs `json:"rootfs"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type dockerRootFs struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
DiffIds []string `json:"diff_ids"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Layer represents a Docker image layer and metadata
|
|
||||||
type dockerLayer struct {
|
|
||||||
tarPath string
|
|
||||||
history dockerImageHistoryEntry
|
|
||||||
index int
|
|
||||||
tree *filetree.FileTree
|
|
||||||
}
|
|
@ -1,8 +1,10 @@
|
|||||||
package runtime
|
package ci
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
|
"github.com/wagoodman/dive/dive/image"
|
||||||
|
"github.com/wagoodman/dive/utils"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -10,7 +12,6 @@ import (
|
|||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"github.com/logrusorgru/aurora"
|
"github.com/logrusorgru/aurora"
|
||||||
"github.com/wagoodman/dive/image"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type CiEvaluator struct {
|
type CiEvaluator struct {
|
||||||
@ -133,7 +134,7 @@ func (ci *CiEvaluator) Evaluate(analysis *image.AnalysisResult) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ci *CiEvaluator) Report() {
|
func (ci *CiEvaluator) Report() {
|
||||||
fmt.Println(title("Inefficient Files:"))
|
fmt.Println(utils.TitleFormat("Inefficient Files:"))
|
||||||
|
|
||||||
template := "%5s %12s %-s\n"
|
template := "%5s %12s %-s\n"
|
||||||
fmt.Printf(template, "Count", "Wasted Space", "File Path")
|
fmt.Printf(template, "Count", "Wasted Space", "File Path")
|
||||||
@ -142,11 +143,11 @@ func (ci *CiEvaluator) Report() {
|
|||||||
fmt.Println("None")
|
fmt.Println("None")
|
||||||
} else {
|
} else {
|
||||||
for _, file := range ci.InefficientFiles {
|
for _, file := range ci.InefficientFiles {
|
||||||
fmt.Printf(template, strconv.Itoa(file.References), humanize.Bytes(uint64(file.SizeBytes)), file.Path)
|
fmt.Printf(template, strconv.Itoa(file.References), humanize.Bytes(file.SizeBytes), file.Path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(title("Results:"))
|
fmt.Println(utils.TitleFormat("Results:"))
|
||||||
|
|
||||||
status := "PASS"
|
status := "PASS"
|
||||||
|
|
@ -1,16 +1,16 @@
|
|||||||
package runtime
|
package ci
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/wagoodman/dive/dive/image/docker"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"github.com/wagoodman/dive/image"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_Evaluator(t *testing.T) {
|
func Test_Evaluator(t *testing.T) {
|
||||||
|
|
||||||
result, err := image.TestLoadDockerImageTar("../.data/test-docker-image.tar")
|
result, err := docker.TestLoadDockerImageTar("../../.data/test-docker-image.tar")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test_Export: unable to fetch analysis: %v", err)
|
t.Fatalf("Test_Export: unable to fetch analysis: %v", err)
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package runtime
|
package ci
|
||||||
|
|
||||||
type ReferenceFile struct {
|
type ReferenceFile struct {
|
||||||
References int `json:"count"`
|
References int `json:"count"`
|
@ -1,14 +1,14 @@
|
|||||||
package runtime
|
package ci
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/wagoodman/dive/dive/image"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/logrusorgru/aurora"
|
"github.com/logrusorgru/aurora"
|
||||||
"github.com/wagoodman/dive/image"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -25,7 +25,7 @@ type CiRule interface {
|
|||||||
Key() string
|
Key() string
|
||||||
Configuration() string
|
Configuration() string
|
||||||
Validate() error
|
Validate() error
|
||||||
Evaluate(*image.AnalysisResult) (RuleStatus, string)
|
Evaluate(result *image.AnalysisResult) (RuleStatus, string)
|
||||||
}
|
}
|
||||||
|
|
||||||
type GenericCiRule struct {
|
type GenericCiRule struct {
|
@ -1,10 +1,9 @@
|
|||||||
package runtime
|
package export
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"github.com/wagoodman/dive/dive/image"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
|
||||||
"github.com/wagoodman/dive/image"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type export struct {
|
type export struct {
|
||||||
@ -20,16 +19,22 @@ type exportLayer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type exportImage struct {
|
type exportImage struct {
|
||||||
SizeBytes uint64 `json:"sizeBytes"`
|
SizeBytes uint64 `json:"sizeBytes"`
|
||||||
InefficientBytes uint64 `json:"inefficientBytes"`
|
InefficientBytes uint64 `json:"inefficientBytes"`
|
||||||
EfficiencyScore float64 `json:"efficiencyScore"`
|
EfficiencyScore float64 `json:"efficiencyScore"`
|
||||||
InefficientFiles []ReferenceFile `json:"ReferenceFile"`
|
InefficientFiles []exportReferenceFile `json:"exportReferenceFile"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func newExport(analysis *image.AnalysisResult) *export {
|
type exportReferenceFile struct {
|
||||||
|
References int `json:"count"`
|
||||||
|
SizeBytes uint64 `json:"sizeBytes"`
|
||||||
|
Path string `json:"file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExport(analysis *image.AnalysisResult) *export {
|
||||||
data := export{}
|
data := export{}
|
||||||
data.Layer = make([]exportLayer, len(analysis.Layers))
|
data.Layer = make([]exportLayer, len(analysis.Layers))
|
||||||
data.Image.InefficientFiles = make([]ReferenceFile, len(analysis.Inefficiencies))
|
data.Image.InefficientFiles = make([]exportReferenceFile, len(analysis.Inefficiencies))
|
||||||
|
|
||||||
// export layers in order
|
// export layers in order
|
||||||
for revIdx := len(analysis.Layers) - 1; revIdx >= 0; revIdx-- {
|
for revIdx := len(analysis.Layers) - 1; revIdx >= 0; revIdx-- {
|
||||||
@ -51,7 +56,7 @@ func newExport(analysis *image.AnalysisResult) *export {
|
|||||||
for idx := 0; idx < len(analysis.Inefficiencies); idx++ {
|
for idx := 0; idx < len(analysis.Inefficiencies); idx++ {
|
||||||
fileData := analysis.Inefficiencies[len(analysis.Inefficiencies)-1-idx]
|
fileData := analysis.Inefficiencies[len(analysis.Inefficiencies)-1-idx]
|
||||||
|
|
||||||
data.Image.InefficientFiles[idx] = ReferenceFile{
|
data.Image.InefficientFiles[idx] = exportReferenceFile{
|
||||||
References: len(fileData.Nodes),
|
References: len(fileData.Nodes),
|
||||||
SizeBytes: uint64(fileData.CumulativeSize),
|
SizeBytes: uint64(fileData.CumulativeSize),
|
||||||
Path: fileData.Path,
|
Path: fileData.Path,
|
||||||
@ -65,7 +70,7 @@ func (exp *export) marshal() ([]byte, error) {
|
|||||||
return json.MarshalIndent(&exp, "", " ")
|
return json.MarshalIndent(&exp, "", " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (exp *export) toFile(exportFilePath string) error {
|
func (exp *export) ToFile(exportFilePath string) error {
|
||||||
payload, err := exp.marshal()
|
payload, err := exp.marshal()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
@ -1,18 +1,17 @@
|
|||||||
package runtime
|
package export
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/wagoodman/dive/dive/image/docker"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/wagoodman/dive/image"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_Export(t *testing.T) {
|
func Test_Export(t *testing.T) {
|
||||||
|
|
||||||
result, err := image.TestLoadDockerImageTar("../.data/test-docker-image.tar")
|
result, err := docker.TestLoadDockerImageTar("../../.data/test-docker-image.tar")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test_Export: unable to fetch analysis: %v", err)
|
t.Fatalf("Test_Export: unable to fetch analysis: %v", err)
|
||||||
}
|
}
|
||||||
export := newExport(result)
|
export := NewExport(result)
|
||||||
payload, err := export.marshal()
|
payload, err := export.marshal()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Test_Export: unable to export analysis: %v", err)
|
t.Errorf("Test_Export: unable to export analysis: %v", err)
|
||||||
@ -109,7 +108,7 @@ func Test_Export(t *testing.T) {
|
|||||||
"sizeBytes": 1220598,
|
"sizeBytes": 1220598,
|
||||||
"inefficientBytes": 32025,
|
"inefficientBytes": 32025,
|
||||||
"efficiencyScore": 0.9844212134184309,
|
"efficiencyScore": 0.9844212134184309,
|
||||||
"ReferenceFile": [
|
"exportReferenceFile": [
|
||||||
{
|
{
|
||||||
"count": 2,
|
"count": 2,
|
||||||
"sizeBytes": 12810,
|
"sizeBytes": 12810,
|
@ -2,28 +2,26 @@ package runtime
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/wagoodman/dive/dive"
|
||||||
|
"github.com/wagoodman/dive/runtime/ci"
|
||||||
|
"github.com/wagoodman/dive/runtime/export"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/logrusorgru/aurora"
|
"github.com/wagoodman/dive/dive/filetree"
|
||||||
"github.com/wagoodman/dive/filetree"
|
"github.com/wagoodman/dive/dive/image"
|
||||||
"github.com/wagoodman/dive/image"
|
"github.com/wagoodman/dive/runtime/ui"
|
||||||
"github.com/wagoodman/dive/ui"
|
|
||||||
"github.com/wagoodman/dive/utils"
|
"github.com/wagoodman/dive/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func title(s string) string {
|
|
||||||
return aurora.Bold(s).String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func runCi(analysis *image.AnalysisResult, options Options) {
|
func runCi(analysis *image.AnalysisResult, options Options) {
|
||||||
fmt.Printf(" efficiency: %2.4f %%\n", analysis.Efficiency*100)
|
fmt.Printf(" efficiency: %2.4f %%\n", analysis.Efficiency*100)
|
||||||
fmt.Printf(" wastedBytes: %d bytes (%s)\n", analysis.WastedBytes, humanize.Bytes(analysis.WastedBytes))
|
fmt.Printf(" wastedBytes: %d bytes (%s)\n", analysis.WastedBytes, humanize.Bytes(analysis.WastedBytes))
|
||||||
fmt.Printf(" userWastedPercent: %2.4f %%\n", analysis.WastedUserPercent*100)
|
fmt.Printf(" userWastedPercent: %2.4f %%\n", analysis.WastedUserPercent*100)
|
||||||
|
|
||||||
evaluator := NewCiEvaluator(options.CiConfig)
|
evaluator := ci.NewCiEvaluator(options.CiConfig)
|
||||||
|
|
||||||
pass := evaluator.Evaluate(analysis)
|
pass := evaluator.Evaluate(analysis)
|
||||||
evaluator.Report()
|
evaluator.Report()
|
||||||
@ -63,13 +61,13 @@ func Run(options Options) {
|
|||||||
doBuild := len(options.BuildArgs) > 0
|
doBuild := len(options.BuildArgs) > 0
|
||||||
|
|
||||||
if doBuild {
|
if doBuild {
|
||||||
fmt.Println(title("Building image..."))
|
fmt.Println(utils.TitleFormat("Building image..."))
|
||||||
options.ImageId = runBuild(options.BuildArgs)
|
options.ImageId = runBuild(options.BuildArgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
analyzer := image.GetAnalyzer(options.ImageId)
|
analyzer := dive.GetAnalyzer(options.ImageId)
|
||||||
|
|
||||||
fmt.Println(title("Fetching image...") + " (this can take a while with large images)")
|
fmt.Println(utils.TitleFormat("Fetching image...") + " (this can take a while with large images)")
|
||||||
reader, err := analyzer.Fetch()
|
reader, err := analyzer.Fetch()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("cannot fetch image: %v\n", err)
|
fmt.Printf("cannot fetch image: %v\n", err)
|
||||||
@ -77,7 +75,7 @@ func Run(options Options) {
|
|||||||
}
|
}
|
||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
|
|
||||||
fmt.Println(title("Parsing image..."))
|
fmt.Println(utils.TitleFormat("Parsing image..."))
|
||||||
err = analyzer.Parse(reader)
|
err = analyzer.Parse(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("cannot parse image: %v\n", err)
|
fmt.Printf("cannot parse image: %v\n", err)
|
||||||
@ -85,9 +83,9 @@ func Run(options Options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if doExport {
|
if doExport {
|
||||||
fmt.Println(title(fmt.Sprintf("Analyzing image... (export to '%s')", options.ExportFile)))
|
fmt.Println(utils.TitleFormat(fmt.Sprintf("Analyzing image... (export to '%s')", options.ExportFile)))
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(title("Analyzing image..."))
|
fmt.Println(utils.TitleFormat("Analyzing image..."))
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := analyzer.Analyze()
|
result, err := analyzer.Analyze()
|
||||||
@ -97,7 +95,7 @@ func Run(options Options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if doExport {
|
if doExport {
|
||||||
err = newExport(result).toFile(options.ExportFile)
|
err = export.NewExport(result).ToFile(options.ExportFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("cannot write export file: %v\n", err)
|
fmt.Printf("cannot write export file: %v\n", err)
|
||||||
utils.Exit(1)
|
utils.Exit(1)
|
||||||
@ -111,7 +109,7 @@ func Run(options Options) {
|
|||||||
utils.Exit(0)
|
utils.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(title("Building cache..."))
|
fmt.Println(utils.TitleFormat("Building cache..."))
|
||||||
cache := filetree.NewFileTreeCache(result.RefTrees)
|
cache := filetree.NewFileTreeCache(result.RefTrees)
|
||||||
cache.Build()
|
cache.Build()
|
||||||
|
|
||||||
|
@ -2,13 +2,13 @@ package ui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/wagoodman/dive/dive/filetree"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/jroimartin/gocui"
|
"github.com/jroimartin/gocui"
|
||||||
"github.com/lunixbochs/vtclean"
|
"github.com/lunixbochs/vtclean"
|
||||||
"github.com/wagoodman/dive/filetree"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DetailsController holds the UI objects and data models for populating the lower-left pane. Specifically the pane that
|
// DetailsController holds the UI objects and data models for populating the lower-left pane. Specifically the pane that
|
@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/wagoodman/keybinding"
|
"github.com/wagoodman/keybinding"
|
||||||
|
|
||||||
"github.com/jroimartin/gocui"
|
"github.com/jroimartin/gocui"
|
||||||
"github.com/wagoodman/dive/filetree"
|
"github.com/wagoodman/dive/dive/filetree"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/wagoodman/dive/utils"
|
"github.com/wagoodman/dive/utils"
|
||||||
|
|
||||||
"github.com/lunixbochs/vtclean"
|
"github.com/lunixbochs/vtclean"
|
||||||
"github.com/wagoodman/dive/filetree"
|
"github.com/wagoodman/dive/dive/filetree"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FileTreeViewModel holds the UI objects and data models for populating the right pane. Specifically the pane that
|
// FileTreeViewModel holds the UI objects and data models for populating the right pane. Specifically the pane that
|
@ -2,6 +2,7 @@ package ui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"github.com/wagoodman/dive/dive/image/docker"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -10,8 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/sergi/go-diff/diffmatchpatch"
|
"github.com/sergi/go-diff/diffmatchpatch"
|
||||||
"github.com/wagoodman/dive/filetree"
|
"github.com/wagoodman/dive/dive/filetree"
|
||||||
"github.com/wagoodman/dive/image"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const allowTestDataCapture = false
|
const allowTestDataCapture = false
|
||||||
@ -73,7 +73,7 @@ func assertTestData(t *testing.T, actualBytes []byte) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func initializeTestViewModel(t *testing.T) *FileTreeViewModel {
|
func initializeTestViewModel(t *testing.T) *FileTreeViewModel {
|
||||||
result, err := image.TestLoadDockerImageTar("../.data/test-docker-image.tar")
|
result, err := docker.TestLoadDockerImageTar("../../.data/test-docker-image.tar")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%s: unable to fetch analysis: %v", t.Name(), err)
|
t.Fatalf("%s: unable to fetch analysis: %v", t.Name(), err)
|
||||||
}
|
}
|
@ -2,13 +2,13 @@ package ui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/wagoodman/dive/dive/image"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/jroimartin/gocui"
|
"github.com/jroimartin/gocui"
|
||||||
"github.com/lunixbochs/vtclean"
|
"github.com/lunixbochs/vtclean"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"github.com/wagoodman/dive/image"
|
|
||||||
"github.com/wagoodman/dive/utils"
|
"github.com/wagoodman/dive/utils"
|
||||||
"github.com/wagoodman/keybinding"
|
"github.com/wagoodman/keybinding"
|
||||||
)
|
)
|
||||||
@ -282,7 +282,6 @@ func (controller *LayerController) Render() error {
|
|||||||
controller.header.Clear()
|
controller.header.Clear()
|
||||||
width, _ := g.Size()
|
width, _ := g.Size()
|
||||||
headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2))
|
headerStr := fmt.Sprintf("[%s]%s\n", title, strings.Repeat("─", width*2))
|
||||||
// headerStr += fmt.Sprintf("Cmp "+image.LayerFormat, "Layer Digest", "Size", "Command")
|
|
||||||
headerStr += fmt.Sprintf("Cmp"+image.LayerFormat, "Size", "Command")
|
headerStr += fmt.Sprintf("Cmp"+image.LayerFormat, "Size", "Command")
|
||||||
_, _ = fmt.Fprintln(controller.header, Formatting.Header(vtclean.Clean(headerStr, false)))
|
_, _ = fmt.Fprintln(controller.header, Formatting.Header(vtclean.Clean(headerStr, false)))
|
||||||
|
|
@ -2,13 +2,13 @@ package ui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/wagoodman/dive/dive/image"
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/jroimartin/gocui"
|
"github.com/jroimartin/gocui"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"github.com/wagoodman/dive/filetree"
|
"github.com/wagoodman/dive/dive/filetree"
|
||||||
"github.com/wagoodman/dive/image"
|
|
||||||
"github.com/wagoodman/dive/utils"
|
"github.com/wagoodman/dive/utils"
|
||||||
"github.com/wagoodman/keybinding"
|
"github.com/wagoodman/keybinding"
|
||||||
)
|
)
|
9
utils/format.go
Normal file
9
utils/format.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/logrusorgru/aurora"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TitleFormat(s string) string {
|
||||||
|
return aurora.Bold(s).String()
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user