Skip to content

Commit

Permalink
v0.0.3 (#3)
Browse files Browse the repository at this point in the history
* Add `--depth` parameter
  • Loading branch information
sergkondr authored Jul 24, 2024
1 parent 284a6cd commit 8d6d262
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 56 deletions.
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
VERSION := 0.0.2
APP_NAME := docker-tree
VERSION := 0.0.3

build: test
go build -ldflags="-X 'main.version=$(VERSION)'" -o docker-tree ./cmd/
go build -ldflags="-X 'main.version=${VERSION}'" -o ${APP_NAME} ./cmd/

test:
go vet ./...
Expand Down
19 changes: 1 addition & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,7 @@ mv ./docker-tree ~/.docker/cli-plugins/docker-tree
```shell
# Absent image will be pulled automatically
➜ docker tree alpine:3.20 /etc/ssl
3.20: Pulling from library/alpine
a258b2a6b59a: Pull complete
Digest: sha256:b89d9c93e9ed3597455c90a0b88a8bbb5cb7188438f70953fede212a0c4394e0
Status: Downloaded newer image for alpine:3.20
docker.io/library/alpine:3.20
processing image: alpine:3.20
ssl/
├── cert.pem
├── certs/
Expand All @@ -35,17 +31,4 @@ ssl/
├── openssl.cnf
├── openssl.cnf.dist
└── private/

# Show file tree with symlinks
➜ docker tree -l alpine:3.20 | head
precessing image: alpine:3.20
/
├── bin/
│ ├── arch -> /bin/busybox
│ ├── ash -> /bin/busybox
│ ├── base64 -> /bin/busybox
│ ├── bbconfig -> /bin/busybox
│ ├── busybox
│ ├── cat -> /bin/busybox
│ ├── chattr -> /bin/busybox
```
9 changes: 9 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"math"
"os"

"github.com/skondrashov/docker-tree/internal/docker"
Expand All @@ -22,6 +23,7 @@ func main() {
var (
quiet bool
showLinks bool
depth int
)

cmd := &cobra.Command{
Expand All @@ -47,11 +49,17 @@ You can also specify a directory to see the file tree relative to this directory
treeRoot = args[1]
}

depth++ // to display not only "/" with --depth == 1
if depth <= 1 {
depth = math.MaxInt
}

treeStrings, err := docker.GetImageTree(docker.GetTreeOpts{
Cli: dockerCli,
ImageID: imageID,
Quiet: quiet,
ShowLinks: showLinks,
Depth: depth,
TreeRoot: treeRoot,
})
if err != nil {
Expand All @@ -64,6 +72,7 @@ You can also specify a directory to see the file tree relative to this directory
}

flags := cmd.Flags()
flags.IntVarP(&depth, "depth", "d", 0, "Show maximum depth of hierarchical trees, 0 - unlimited")
flags.BoolVarP(&quiet, "quiet", "q", false, "Suppress verbose output")
flags.BoolVarP(&showLinks, "links", "l", false, "Show symlinks destination")

Expand Down
5 changes: 4 additions & 1 deletion internal/docker/layers.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,10 @@ func getFileTreeFromLayer(layerReader *tar.Reader) (*fileTreeNode, error) {
if err != nil {
return nil, errNotATar
}
fileTree.addChild(header)

if !strings.HasSuffix(header.Name, whiteoutDirPrefix) {
fileTree.addChild(header)
}
}
return fileTree, nil
}
10 changes: 8 additions & 2 deletions internal/docker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type GetTreeOpts struct {
ImageID string
Quiet bool
ShowLinks bool
Depth int
TreeRoot string
}

Expand All @@ -36,7 +37,7 @@ func GetImageTree(opts GetTreeOpts) (string, error) {
}

if !opts.Quiet {
fmt.Fprintf(opts.Cli.Out(), "precessing image: %s\n", opts.ImageID)
fmt.Fprintf(opts.Cli.Out(), "processing image: %s\n", opts.ImageID)
}

imageReader, err := opts.Cli.Client().ImageSave(ctx, []string{opts.ImageID})
Expand Down Expand Up @@ -64,5 +65,10 @@ func GetImageTree(opts GetTreeOpts) (string, error) {
return "", fmt.Errorf("there is no such path in the image: %s", opts.TreeRoot)
}

return node.getString("", opts.ShowLinks, true, true), nil
printOptions := getStringOpts{
showLinks: opts.ShowLinks,
depth: opts.Depth,
}

return node.getString("", printOptions, true, true), nil
}
23 changes: 17 additions & 6 deletions internal/docker/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ const (
last = "└── "
link = " -> "

delFilePrefix = ".wh."
whiteoutFilePrefix = ".wh."
whiteoutDirPrefix = ".wh..wh..opq"
)

type fileTreeNode struct {
Expand All @@ -25,7 +26,17 @@ type fileTreeNode struct {
Children []*fileTreeNode
}

func (n *fileTreeNode) getString(prefix string, showLinks, isFirst, isLast bool) string {
type getStringOpts struct {
showLinks bool
depth int
}

func (n *fileTreeNode) getString(prefix string, opts getStringOpts, isFirst, isLast bool) string {
opts.depth--
if opts.depth == -1 {
return empty
}

passPrefix := prefix
currentPrefix := empty

Expand All @@ -45,12 +56,12 @@ func (n *fileTreeNode) getString(prefix string, showLinks, isFirst, isLast bool)
}

result := fmt.Sprintf("%s%s%s\n", prefix, currentPrefix, name)
if showLinks && n.Symlink != "" {
if opts.showLinks && n.Symlink != "" {
result = fmt.Sprintf("%s%s%s%s%s\n", prefix, currentPrefix, name, link, n.Symlink)
}

for i, child := range n.Children {
result += child.getString(passPrefix, showLinks, false, i == len(n.Children)-1)
result += child.getString(passPrefix, opts, false, i == len(n.Children)-1)
}

return result
Expand Down Expand Up @@ -127,8 +138,8 @@ func mergeFileTrees(original, updated *fileTreeNode) (*fileTreeNode, error) {
continue
}

if strings.HasPrefix(updatedChild.Name, delFilePrefix) {
updatedChild.Name = strings.TrimPrefix(updatedChild.Name, delFilePrefix)
if strings.HasPrefix(updatedChild.Name, whiteoutFilePrefix) {
updatedChild.Name = strings.TrimPrefix(updatedChild.Name, whiteoutFilePrefix)
if err := original.deleteNode(updatedChild); err != nil {
return nil, fmt.Errorf("error deleting file %s: %w", updatedChild.Name, err)
}
Expand Down
103 changes: 76 additions & 27 deletions internal/docker/tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,45 @@ func Test_fileTreeNode_String(t *testing.T) {
tests := []struct {
name string
fields fields
opts getStringOpts
want string
}{
{
name: "get string of only root node",
fields: fields{"/", true, nil},
opts: getStringOpts{showLinks: false, depth: 99999},
want: "/\n",
},
{
name: "get string of /etc/file",
fields: fields{"/", true, []*fileTreeNode{&etcNode}},
opts: getStringOpts{showLinks: false, depth: 99999},
want: "/\n└── etc/\n └── file\n",
},
{
name: "get string of /etc/file + /other_file",
fields: fields{"/", true, []*fileTreeNode{&etcNode, &otherFileNode}},
opts: getStringOpts{showLinks: false, depth: 99999},
want: "/\n├── etc/\n│ └── file\n└── other_file\n",
},
{
name: "get string with symlink",
fields: fields{"/", true, []*fileTreeNode{&etcNode, &binNodeWithSymlink}},
opts: getStringOpts{showLinks: true, depth: 99999},
want: "/\n├── etc/\n│ └── file\n└── bin/\n ├── file\n └── link -> /tmp/file\n",
},
{
name: "get string with depth = 1",
fields: fields{"/", true, []*fileTreeNode{&etcNode, &binNodeWithSymlink}},
opts: getStringOpts{showLinks: false, depth: 2}, // we use depth == 2 because we want it to handle root + one more level of nesting
want: "/\n├── etc/\n└── bin/\n",
},
{
name: "get string with depth = 2",
fields: fields{"/", true, []*fileTreeNode{&etcNode, &binNodeWithSymlink}},
opts: getStringOpts{showLinks: false, depth: 3},
want: "/\n├── etc/\n│ └── file\n└── bin/\n ├── file\n └── link\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -50,28 +67,15 @@ func Test_fileTreeNode_String(t *testing.T) {
IsDir: tt.fields.IsDir,
Children: tt.fields.Children,
}
if got := n.getString("", true, true, true); got != tt.want {

if got := n.getString("", tt.opts, true, true); got != tt.want {
t.Errorf("getString() = %v, want %v", got, tt.want)
}
})
}
}

func Test_mergeFileTrees(t *testing.T) {
singleFileTree := &fileTreeNode{"file", "", false, nil}

etcWithFile := &fileTreeNode{"etc", "", true, []*fileTreeNode{singleFileTree}}
rootWithEtcTreeNode := &fileTreeNode{"/", "", true, []*fileTreeNode{etcWithFile}}

varWithFile := &fileTreeNode{"var", "", true, []*fileTreeNode{singleFileTree}}
rootWithVarTreeNode := &fileTreeNode{"/", "", true, []*fileTreeNode{varWithFile}}

deleteSingleFileTree := &fileTreeNode{".wh.file", "", false, nil}
etcWithDeleteFile := &fileTreeNode{"etc", "", true, []*fileTreeNode{deleteSingleFileTree}}
rootWithEtcWithDeleteFileTreeNode := &fileTreeNode{"/", "", true, []*fileTreeNode{etcWithDeleteFile}}

rootWithEtcWithDeleteFileAndAddVarFileTreeNode := &fileTreeNode{"/", "", true, []*fileTreeNode{etcWithDeleteFile, varWithFile}}

type args struct {
original *fileTreeNode
updated *fileTreeNode
Expand All @@ -86,40 +90,83 @@ func Test_mergeFileTrees(t *testing.T) {
name: "original is nil",
args: args{
original: nil,
updated: singleFileTree,
updated: &fileTreeNode{"file", "", false, nil},
},
want: singleFileTree,
want: &fileTreeNode{"file", "", false, nil},
wantErr: false,
},
{
name: "add /var/file to /etc/file",
args: args{
original: rootWithEtcTreeNode,
updated: rootWithVarTreeNode,
original: &fileTreeNode{"/", "", true, []*fileTreeNode{
{"etc", "", true, []*fileTreeNode{
{"file", "", false, nil},
}},
}},
updated: &fileTreeNode{"/", "", true, []*fileTreeNode{
{"var", "", true, []*fileTreeNode{
{"file", "", false, nil},
}},
}},
},
want: &fileTreeNode{"/", "", true, []*fileTreeNode{etcWithFile, varWithFile}},
want: &fileTreeNode{"/", "", true, []*fileTreeNode{
{"etc", "", true, []*fileTreeNode{
{"file", "", false, nil}},
},
{"var", "", true, []*fileTreeNode{
{"file", "", false, nil}},
},
}},

wantErr: false,
},
{
name: "delete /etc/file",
args: args{
original: rootWithEtcTreeNode,
updated: rootWithEtcWithDeleteFileTreeNode,
original: &fileTreeNode{"/", "", true, []*fileTreeNode{
{"etc", "", true, []*fileTreeNode{
{"file", "", false, nil},
}},
}},
updated: &fileTreeNode{"/", "", true, []*fileTreeNode{
{"etc", "", true, []*fileTreeNode{
{".wh.file", "", false, nil},
}},
}},
},
want: &fileTreeNode{"/", "", true, []*fileTreeNode{{"etc", "", true, []*fileTreeNode{}}}},
want: &fileTreeNode{"/", "", true, []*fileTreeNode{
{"etc", "", true, []*fileTreeNode{}},
}},
wantErr: false,
},
{
name: "delete /etc/file and add /var/file",
args: args{
original: rootWithEtcTreeNode,
updated: rootWithEtcWithDeleteFileAndAddVarFileTreeNode,
original: &fileTreeNode{"/", "", true, []*fileTreeNode{
{"etc", "", true, []*fileTreeNode{
{"file", "", false, nil},
}},
}},
updated: &fileTreeNode{"/", "", true, []*fileTreeNode{
{"etc", "", true, []*fileTreeNode{
{".wh.file", "", false, nil},
}},
{"var", "", false, []*fileTreeNode{
{"file", "", false, nil},
}},
}},
},
want: &fileTreeNode{"/", "", true, []*fileTreeNode{{"etc", "", true, []*fileTreeNode{}}, varWithFile}},
want: &fileTreeNode{"/", "", true, []*fileTreeNode{
{"etc", "", true, []*fileTreeNode{}},
{"var", "", false, []*fileTreeNode{
{"file", "", false, nil},
}},
}},
wantErr: false,
},
}

defaultOpts := getStringOpts{showLinks: true, depth: 99999}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := mergeFileTrees(tt.args.original, tt.args.updated)
Expand All @@ -128,7 +175,9 @@ func Test_mergeFileTrees(t *testing.T) {
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("mergeFileTrees() got:\n%v, want:\n%v", got, tt.want)
t.Errorf("mergeFileTrees() got:\n%v, want:\n%v",
got.getString("", defaultOpts, true, false),
tt.want.getString("", defaultOpts, true, false))
}
})
}
Expand Down

0 comments on commit 8d6d262

Please sign in to comment.