diff --git a/README.md b/README.md index 2ec1e9d..67de18a 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,48 @@ [![license](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![go report card](https://goreportcard.com/badge/github.com/dguerri/dockerfuse)](https://goreportcard.com/report/github.com/dguerri/dockerfuse) [![CI](https://github.com/dguerri/dockerfuse/actions/workflows/run-CI.yml/badge.svg)](https://github.com/dguerri/dockerfuse/actions/workflows/run-CI.yml) [![coverage status](https://coveralls.io/repos/github/dguerri/dockerfuse/badge.svg?branch=main)](https://coveralls.io/github/dguerri/dockerfuse?branch=main) -***NOTE: this software is a WIP, use at your risk!*** +**NOTE: this software is a WIP, use at your risk!** -DockerFuse allows mounting the filesystem of Linux Docker containers locally, without installing additional services on the container (e.g. ssh). +DockerFuse lets you mount the filesystem of Linux Docker containers locally without installing additional services on the container. ![dockerfuse demo](doc/dockerfuse.gif) +## Build + +DockerFuse is built using the provided Makefile. Running `make all` compiles the main `dockerfuse` binary and the architecture specific satellites used inside the container: + +```bash +make all +``` + +The resulting files are `dockerfuse`, `dockerfuse_satellite_amd64` and `dockerfuse_satellite_arm64`. + +## Running + +Mount the root filesystem of a running container with: + +```bash +sudo ./dockerfuse -i -m +``` + +Specify `-path` to mount a sub directory and `-daemonize` to keep the process in the background. +DockerFuse can connect to remote Docker engines using the standard `DOCKER_HOST` environment variables. + +## Makefile targets + +- `make test` – run unit tests. +- `make quality_test` – run go vet, unit tests with coverage, golint and gocyclo. +- `make interactive_test` – pull the alpine image and mount it under `./tmp` for a quick demo. + ## Testing -To run Unit tests (very few for now): +To run the unit tests: ```bash make test ``` -To run an interactive test, pulling `alpine` image, spinning up a container and mounting its filesystem on ./tmp: +To run an interactive test that spawns an `alpine` container and mounts it under `./tmp`: ```bash make interactive_test @@ -78,6 +105,14 @@ Yup! Matter of fact Dockerfuse works great on minimal Docker containers, even wh Nope. Although it shouldn't be to hard to code, there is no support for Windows containers at this time. +### Q. Does it require root privileges? + +Yes. Dockerfuse mounts the filesystem directly using FUSE and therefore needs privileges to perform the mount operation. Run it with `sudo` or as a user allowed to mount FUSE filesystems. + +### Q. Can I mount only a sub directory of a container? + +Absolutely. Use the `-path` option to specify the directory inside the container that you want to mount. + ## License Apache License v2. See LICENSE.TXT for details. diff --git a/cmd/dockerfuse/client/client_ops_test.go b/cmd/dockerfuse/client/client_ops_test.go deleted file mode 100644 index d3cf3c3..0000000 --- a/cmd/dockerfuse/client/client_ops_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package client - -import ( - "context" - "fmt" - "syscall" - "testing" - - "github.com/dguerri/dockerfuse/pkg/rpccommon" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func TestDockerFuseClientStat(t *testing.T) { - var mRPCC mockRPCClient - fdc := &DockerFuseClient{rpcClient: &mRPCC} - - expected := rpccommon.StatReply{ - Mode: 0755, - Nlink: 1, - Ino: 42, - UID: 1000, - GID: 1000, - Atime: 1, - Mtime: 2, - Ctime: 3, - Size: 64, - Blocks: 1, - Blksize: 4096, - LinkTarget: "link", - } - - mRPCC.On("Call", "DockerFuseFSOps.Stat", rpccommon.StatRequest{FullPath: "/test"}, mock.Anything). - Run(func(args mock.Arguments) { - reply := args.Get(2).(*rpccommon.StatReply) - *reply = expected - }).Return(nil) - - var attr statAttr - errno := fdc.stat(context.Background(), "/test", &attr) - - assert.Equal(t, syscall.Errno(0), errno) - assert.Equal(t, expected.Ino, attr.FuseAttr.Ino) - assert.Equal(t, uint64(expected.Size), attr.FuseAttr.Size) - assert.Equal(t, uint64(expected.Blocks), attr.FuseAttr.Blocks) - assert.Equal(t, expected.Mode, attr.FuseAttr.Mode) - assert.Equal(t, expected.Nlink, attr.FuseAttr.Nlink) - assert.Equal(t, expected.UID, attr.FuseAttr.Owner.Uid) - assert.Equal(t, expected.GID, attr.FuseAttr.Owner.Gid) - assert.Equal(t, expected.LinkTarget, attr.LinkTarget) - - mRPCC.AssertExpectations(t) -} - -func TestDockerFuseClientStatError(t *testing.T) { - var mRPCC mockRPCClient - fdc := &DockerFuseClient{rpcClient: &mRPCC} - - mRPCC.On("Call", "DockerFuseFSOps.Stat", rpccommon.StatRequest{FullPath: "/enoent"}, mock.Anything). - Return(fmt.Errorf("errno: ENOENT")) - - var attr statAttr - errno := fdc.stat(context.Background(), "/enoent", &attr) - - assert.Equal(t, syscall.ENOENT, errno) - mRPCC.AssertExpectations(t) -} - -func TestDockerFuseClientReadDir(t *testing.T) { - var mRPCC mockRPCClient - fdc := &DockerFuseClient{rpcClient: &mRPCC} - - reply := rpccommon.ReadDirReply{DirEntries: []rpccommon.DirEntry{ - {Ino: 1, Name: "."}, - {Ino: 2, Name: ".."}, - {Ino: 3, Name: "file", Mode: 0644}, - {Ino: 4, Name: "dir", Mode: 0755}, - }} - - mRPCC.On("Call", "DockerFuseFSOps.ReadDir", rpccommon.StatRequest{FullPath: "/dir"}, mock.Anything). - Run(func(args mock.Arguments) { - r := args.Get(2).(*rpccommon.ReadDirReply) - *r = reply - }).Return(nil) - - ds, errno := fdc.readDir(context.Background(), "/dir") - - assert.Equal(t, syscall.Errno(0), errno) - - var got []string - for ds.HasNext() { - e, _ := ds.Next() - got = append(got, e.Name) - } - - assert.Equal(t, []string{"file", "dir"}, got) - mRPCC.AssertExpectations(t) -} - -func TestDockerFuseClientReadDirError(t *testing.T) { - var mRPCC mockRPCClient - fdc := &DockerFuseClient{rpcClient: &mRPCC} - - mRPCC.On("Call", "DockerFuseFSOps.ReadDir", rpccommon.StatRequest{FullPath: "/err"}, mock.Anything). - Return(fmt.Errorf("errno: EACCES")) - - _, errno := fdc.readDir(context.Background(), "/err") - - assert.Equal(t, syscall.EACCES, errno) - mRPCC.AssertExpectations(t) -} diff --git a/cmd/dockerfuse/client/client_test.go b/cmd/dockerfuse/client/client_test.go index 66b1a6e..0943cd7 100644 --- a/cmd/dockerfuse/client/client_test.go +++ b/cmd/dockerfuse/client/client_test.go @@ -4,12 +4,17 @@ import ( "context" "fmt" "io" + "io/fs" "path/filepath" + "syscall" "testing" + "github.com/dguerri/dockerfuse/pkg/rpccommon" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" + fusefs "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -375,3 +380,199 @@ func TestNewFuseDockerClient(t *testing.T) { mDCF.AssertExpectations(t) } + +func TestDockerFuseClientStat(t *testing.T) { + var mRPCC mockRPCClient + fdc := &DockerFuseClient{rpcClient: &mRPCC} + + expected := rpccommon.StatReply{ + Mode: 0755, + Nlink: 1, + Ino: 42, + UID: 1000, + GID: 1000, + Atime: 1, + Mtime: 2, + Ctime: 3, + Size: 64, + Blocks: 1, + Blksize: 4096, + LinkTarget: "link", + } + + mRPCC.On("Call", "DockerFuseFSOps.Stat", rpccommon.StatRequest{FullPath: "/test"}, mock.Anything). + Run(func(args mock.Arguments) { + reply := args.Get(2).(*rpccommon.StatReply) + *reply = expected + }).Return(nil) + + var attr statAttr + errno := fdc.stat(context.Background(), "/test", &attr) + + assert.Equal(t, syscall.Errno(0), errno) + assert.Equal(t, expected.Ino, attr.FuseAttr.Ino) + assert.Equal(t, uint64(expected.Size), attr.FuseAttr.Size) + assert.Equal(t, uint64(expected.Blocks), attr.FuseAttr.Blocks) + assert.Equal(t, expected.Mode, attr.FuseAttr.Mode) + assert.Equal(t, expected.Nlink, attr.FuseAttr.Nlink) + assert.Equal(t, expected.UID, attr.FuseAttr.Owner.Uid) + assert.Equal(t, expected.GID, attr.FuseAttr.Owner.Gid) + assert.Equal(t, expected.LinkTarget, attr.LinkTarget) + + mRPCC.AssertExpectations(t) +} + +func TestDockerFuseClientStatError(t *testing.T) { + var mRPCC mockRPCClient + fdc := &DockerFuseClient{rpcClient: &mRPCC} + + mRPCC.On("Call", "DockerFuseFSOps.Stat", rpccommon.StatRequest{FullPath: "/enoent"}, mock.Anything). + Return(fmt.Errorf("errno: ENOENT")) + + var attr statAttr + errno := fdc.stat(context.Background(), "/enoent", &attr) + + assert.Equal(t, syscall.ENOENT, errno) + mRPCC.AssertExpectations(t) +} + +func TestDockerFuseClientReadDir(t *testing.T) { + var mRPCC mockRPCClient + fdc := &DockerFuseClient{rpcClient: &mRPCC} + + reply := rpccommon.ReadDirReply{DirEntries: []rpccommon.DirEntry{ + {Ino: 1, Name: "."}, + {Ino: 2, Name: ".."}, + {Ino: 3, Name: "file", Mode: 0644}, + {Ino: 4, Name: "dir", Mode: 0755}, + }} + + mRPCC.On("Call", "DockerFuseFSOps.ReadDir", rpccommon.StatRequest{FullPath: "/dir"}, mock.Anything). + Run(func(args mock.Arguments) { + r := args.Get(2).(*rpccommon.ReadDirReply) + *r = reply + }).Return(nil) + + ds, errno := fdc.readDir(context.Background(), "/dir") + + assert.Equal(t, syscall.Errno(0), errno) + + var got []string + for ds.HasNext() { + e, _ := ds.Next() + got = append(got, e.Name) + } + + assert.Equal(t, []string{"file", "dir"}, got) + mRPCC.AssertExpectations(t) +} + +func TestDockerFuseClientReadDirError(t *testing.T) { + var mRPCC mockRPCClient + fdc := &DockerFuseClient{rpcClient: &mRPCC} + + mRPCC.On("Call", "DockerFuseFSOps.ReadDir", rpccommon.StatRequest{FullPath: "/err"}, mock.Anything). + Return(fmt.Errorf("errno: EACCES")) + + _, errno := fdc.readDir(context.Background(), "/err") + + assert.Equal(t, syscall.EACCES, errno) + mRPCC.AssertExpectations(t) +} + +func TestDockerFuseClientCreate(t *testing.T) { + var mRPCC mockRPCClient + fdc := &DockerFuseClient{rpcClient: &mRPCC} + mRPCC.On("Call", "DockerFuseFSOps.Open", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + reply := args.Get(2).(*rpccommon.OpenReply) + *reply = rpccommon.OpenReply{FD: 1, StatReply: rpccommon.StatReply{Mode: 0644}} + }).Return(nil) + var attr statAttr + fh, err := fdc.create(context.Background(), "/f", 0, 0644, &attr) + assert.Equal(t, fusefs.FileHandle(uintptr(1)), fh) + assert.Equal(t, syscall.Errno(0), err) + assert.Equal(t, uint32(0644), attr.FuseAttr.Mode) + mRPCC.AssertExpectations(t) +} + +func TestDockerFuseClientOpenClose(t *testing.T) { + var mRPCC mockRPCClient + fdc := &DockerFuseClient{rpcClient: &mRPCC} + mRPCC.On("Call", "DockerFuseFSOps.Open", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + reply := args.Get(2).(*rpccommon.OpenReply) + *reply = rpccommon.OpenReply{FD: 2, StatReply: rpccommon.StatReply{Mode: 0600}} + }).Return(nil) + fh, mode, err := fdc.open(context.Background(), "/f", 0, 0) + assert.Equal(t, fusefs.FileHandle(uintptr(2)), fh) + assert.Equal(t, fs.FileMode(0600), mode) + assert.Equal(t, syscall.Errno(0), err) + mRPCC.On("Call", "DockerFuseFSOps.Close", rpccommon.CloseRequest{FD: fh.(uintptr)}, mock.Anything).Return(nil) + cerr := fdc.close(context.Background(), fh) + assert.Equal(t, syscall.Errno(0), cerr) + mRPCC.AssertExpectations(t) +} + +func TestDockerFuseClientReadSeekWrite(t *testing.T) { + var mRPCC mockRPCClient + fdc := &DockerFuseClient{rpcClient: &mRPCC} + mRPCC.On("Call", "DockerFuseFSOps.Read", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + r := args.Get(2).(*rpccommon.ReadReply) + *r = rpccommon.ReadReply{Data: []byte("a")} + }).Return(nil) + data, err := fdc.read(context.Background(), fusefs.FileHandle(uintptr(1)), 0, 1) + assert.Equal(t, []byte("a"), data) + assert.Equal(t, syscall.Errno(0), err) + mRPCC.On("Call", "DockerFuseFSOps.Seek", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + r := args.Get(2).(*rpccommon.SeekReply) + *r = rpccommon.SeekReply{Num: 3} + }).Return(nil) + n, serr := fdc.seek(context.Background(), fusefs.FileHandle(uintptr(1)), 3, 0) + assert.Equal(t, int64(3), n) + assert.Equal(t, syscall.Errno(0), serr) + mRPCC.On("Call", "DockerFuseFSOps.Write", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + r := args.Get(2).(*rpccommon.WriteReply) + *r = rpccommon.WriteReply{Num: 1} + }).Return(nil) + wn, werr := fdc.write(context.Background(), fusefs.FileHandle(uintptr(1)), 0, []byte("a")) + assert.Equal(t, 1, wn) + assert.Equal(t, syscall.Errno(0), werr) + mRPCC.AssertExpectations(t) +} + +func TestDockerFuseClientOtherOps(t *testing.T) { + var m mockRPCClient + fdc := &DockerFuseClient{rpcClient: &m} + m.On("Call", "DockerFuseFSOps.Unlink", rpccommon.UnlinkRequest{FullPath: "/a"}, mock.Anything).Return(nil) + assert.Equal(t, syscall.Errno(0), fdc.unlink(context.Background(), "/a")) + m.On("Call", "DockerFuseFSOps.Fsync", mock.Anything, mock.Anything).Return(nil) + assert.Equal(t, syscall.Errno(0), fdc.fsync(context.Background(), fusefs.FileHandle(uintptr(1)), 0)) + m.On("Call", "DockerFuseFSOps.Mkdir", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + r := args.Get(2).(*rpccommon.MkdirReply) + *r = rpccommon.MkdirReply{Ino: 1} + }).Return(nil) + var attr statAttr + assert.Equal(t, syscall.Errno(0), fdc.mkdir(context.Background(), "/d", 0755, &attr)) + m.On("Call", "DockerFuseFSOps.Rmdir", rpccommon.RmdirRequest{FullPath: "/d"}, mock.Anything).Return(nil) + assert.Equal(t, syscall.Errno(0), fdc.rmdir(context.Background(), "/d")) + m.On("Call", "DockerFuseFSOps.Rename", rpccommon.RenameRequest{FullPath: "/a", FullNewPath: "/b", Flags: 0}, mock.Anything).Return(nil) + assert.Equal(t, syscall.Errno(0), fdc.rename(context.Background(), "/a", "/b", 0)) + m.On("Call", "DockerFuseFSOps.Readlink", rpccommon.ReadlinkRequest{FullPath: "/l"}, mock.Anything).Run(func(args mock.Arguments) { + r := args.Get(2).(*rpccommon.ReadlinkReply) + *r = rpccommon.ReadlinkReply{LinkTarget: "t"} + }).Return(nil) + link, err := fdc.readlink(context.Background(), "/l") + assert.Equal(t, []byte("t"), link) + assert.Equal(t, syscall.Errno(0), err) + m.On("Call", "DockerFuseFSOps.Link", rpccommon.LinkRequest{OldFullPath: "/o", NewFullPath: "/n"}, mock.Anything).Return(nil) + assert.Equal(t, syscall.Errno(0), fdc.link(context.Background(), "/o", "/n")) + m.On("Call", "DockerFuseFSOps.Symlink", rpccommon.SymlinkRequest{OldFullPath: "/o", NewFullPath: "/s"}, mock.Anything).Return(nil) + assert.Equal(t, syscall.Errno(0), fdc.symlink(context.Background(), "/o", "/s")) + m.On("Call", "DockerFuseFSOps.SetAttr", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + r := args.Get(2).(*rpccommon.SetAttrReply) + *r = rpccommon.SetAttrReply{Ino: 5} + }).Return(nil) + var out statAttr + in := &fuse.SetAttrIn{SetAttrInCommon: fuse.SetAttrInCommon{Valid: fuse.FATTR_SIZE, Size: 1}} + assert.Equal(t, syscall.Errno(0), fdc.setAttr(context.Background(), "/file", in, &out)) + m.AssertExpectations(t) +} diff --git a/cmd/dockerfuse/client/dockerfuse_fs_test.go b/cmd/dockerfuse/client/dockerfuse_fs_test.go new file mode 100644 index 0000000..1ad9ad9 --- /dev/null +++ b/cmd/dockerfuse/client/dockerfuse_fs_test.go @@ -0,0 +1,319 @@ +package client + +import ( + "context" + "io/fs" + "syscall" + "testing" + + fusefs "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +type mockFuseDockerClient struct{ mock.Mock } + +func (m *mockFuseDockerClient) disconnect() { + m.Called() +} + +func (m *mockFuseDockerClient) connectSatellite(ctx context.Context) error { + args := m.Called(ctx) + return args.Error(0) +} + +func (m *mockFuseDockerClient) close(ctx context.Context, fh fusefs.FileHandle) syscall.Errno { + args := m.Called(ctx, fh) + if val, ok := args.Get(0).(syscall.Errno); ok { + return val + } + return 0 +} + +func (m *mockFuseDockerClient) create(ctx context.Context, fullPath string, flags int, mode fs.FileMode, attr *statAttr) (fusefs.FileHandle, syscall.Errno) { + args := m.Called(ctx, fullPath, flags, mode, attr) + return args.Get(0).(fusefs.FileHandle), args.Get(1).(syscall.Errno) +} + +func (m *mockFuseDockerClient) fsync(ctx context.Context, fh fusefs.FileHandle, flags uint32) syscall.Errno { + args := m.Called(ctx, fh, flags) + return args.Get(0).(syscall.Errno) +} + +func (m *mockFuseDockerClient) link(ctx context.Context, oldFullPath, newFullPath string) syscall.Errno { + args := m.Called(ctx, oldFullPath, newFullPath) + return args.Get(0).(syscall.Errno) +} + +func (m *mockFuseDockerClient) mkdir(ctx context.Context, fullPath string, mode fs.FileMode, attr *statAttr) syscall.Errno { + args := m.Called(ctx, fullPath, mode, attr) + return args.Get(0).(syscall.Errno) +} + +func (m *mockFuseDockerClient) open(ctx context.Context, fullPath string, flags int, mode fs.FileMode) (fusefs.FileHandle, fs.FileMode, syscall.Errno) { + args := m.Called(ctx, fullPath, flags, mode) + return args.Get(0).(fusefs.FileHandle), args.Get(1).(fs.FileMode), args.Get(2).(syscall.Errno) +} + +func (m *mockFuseDockerClient) read(ctx context.Context, fh fusefs.FileHandle, offset int64, n int) ([]byte, syscall.Errno) { + args := m.Called(ctx, fh, offset, n) + return args.Get(0).([]byte), args.Get(1).(syscall.Errno) +} + +func (m *mockFuseDockerClient) readDir(ctx context.Context, fullPath string) (fusefs.DirStream, syscall.Errno) { + args := m.Called(ctx, fullPath) + return args.Get(0).(fusefs.DirStream), args.Get(1).(syscall.Errno) +} + +func (m *mockFuseDockerClient) readlink(ctx context.Context, fullPath string) ([]byte, syscall.Errno) { + args := m.Called(ctx, fullPath) + return args.Get(0).([]byte), args.Get(1).(syscall.Errno) +} + +func (m *mockFuseDockerClient) rename(ctx context.Context, fullPath, fullNewPath string, flags uint32) syscall.Errno { + args := m.Called(ctx, fullPath, fullNewPath, flags) + return args.Get(0).(syscall.Errno) +} + +func (m *mockFuseDockerClient) rmdir(ctx context.Context, fullPath string) syscall.Errno { + args := m.Called(ctx, fullPath) + return args.Get(0).(syscall.Errno) +} + +func (m *mockFuseDockerClient) seek(ctx context.Context, fh fusefs.FileHandle, offset int64, whence int) (int64, syscall.Errno) { + args := m.Called(ctx, fh, offset, whence) + return args.Get(0).(int64), args.Get(1).(syscall.Errno) +} + +func (m *mockFuseDockerClient) setAttr(ctx context.Context, fullPath string, in *fuse.SetAttrIn, out *statAttr) syscall.Errno { + args := m.Called(ctx, fullPath, in, out) + return args.Get(0).(syscall.Errno) +} + +func (m *mockFuseDockerClient) stat(ctx context.Context, fullPath string, attr *statAttr) syscall.Errno { + args := m.Called(ctx, fullPath, attr) + return args.Get(0).(syscall.Errno) +} + +func (m *mockFuseDockerClient) symlink(ctx context.Context, oldFullPath, newFullPath string) syscall.Errno { + args := m.Called(ctx, oldFullPath, newFullPath) + return args.Get(0).(syscall.Errno) +} + +func (m *mockFuseDockerClient) unlink(ctx context.Context, fullPath string) syscall.Errno { + args := m.Called(ctx, fullPath) + return args.Get(0).(syscall.Errno) +} + +func (m *mockFuseDockerClient) write(ctx context.Context, fh fusefs.FileHandle, offset int64, data []byte) (int, syscall.Errno) { + args := m.Called(ctx, fh, offset, data) + return args.Int(0), args.Get(1).(syscall.Errno) +} + +func TestNodeGetattrSuccess(t *testing.T) { + var m mockFuseDockerClient + n := NewNode(&m, "/path", "") + m.On("stat", mock.Anything, "/path", mock.Anything).Run(func(args mock.Arguments) { + attr := args.Get(2).(*statAttr) + attr.FuseAttr = fuse.Attr{Ino: 10, Mode: fuse.S_IFREG, Size: 42} + }).Return(syscall.Errno(0)) + + var out fuse.AttrOut + errno := n.Getattr(context.Background(), nil, &out) + assert.Equal(t, syscall.Errno(0), errno) + assert.Equal(t, uint64(10), out.Attr.Ino) + assert.Equal(t, uint64(42), out.Attr.Size) + m.AssertExpectations(t) +} + +func TestNodeGetattrError(t *testing.T) { + var m mockFuseDockerClient + n := NewNode(&m, "/path", "") + m.On("stat", mock.Anything, "/path", mock.Anything).Return(syscall.ENOENT) + + var out fuse.AttrOut + errno := n.Getattr(context.Background(), nil, &out) + assert.Equal(t, syscall.ENOENT, errno) + m.AssertExpectations(t) +} + +func TestNodeOpenAndRead(t *testing.T) { + var m mockFuseDockerClient + n := NewNode(&m, "/file", "") + handle := fusefs.FileHandle(uintptr(1)) + m.On("open", mock.Anything, "/file", 0, fs.FileMode(0)).Return(handle, fs.FileMode(0644), syscall.Errno(0)) + fh, mode, err := n.Open(context.Background(), 0) + assert.Equal(t, syscall.Errno(0), err) + assert.Equal(t, handle, fh) + assert.Equal(t, uint32(0644), mode) + + buf := make([]byte, 3) + m.On("read", mock.Anything, handle, int64(0), len(buf)).Return([]byte("abc"), syscall.Errno(0)) + res, rerr := n.Read(context.Background(), handle, buf, 0) + assert.Equal(t, syscall.Errno(0), rerr) + data, _ := res.Bytes([]byte{}) + assert.Equal(t, []byte("abc"), data) + + m.AssertExpectations(t) +} + +func TestNodeReadlinkReaddir(t *testing.T) { + var m mockFuseDockerClient + n := NewNode(&m, "/dir", "") + m.On("readlink", mock.Anything, "/dir").Return([]byte("/target"), syscall.Errno(0)) + link, err := n.Readlink(context.Background()) + assert.Equal(t, []byte("/target"), link) + assert.Equal(t, syscall.Errno(0), err) + + dirEntries := []fuse.DirEntry{{Name: "f1"}, {Name: "f2"}} + m.On("readDir", mock.Anything, "/dir").Return(fusefs.NewListDirStream(dirEntries), syscall.Errno(0)) + ds, derr := n.Readdir(context.Background()) + assert.Equal(t, syscall.Errno(0), derr) + names := []string{} + for ds.HasNext() { + e, _ := ds.Next() + names = append(names, e.Name) + } + assert.Equal(t, []string{"f1", "f2"}, names) + m.AssertExpectations(t) +} + +func TestNodeWriteError(t *testing.T) { + var m mockFuseDockerClient + n := NewNode(&m, "/file", "") + handle := fusefs.FileHandle(uintptr(1)) + m.On("write", mock.Anything, handle, int64(0), []byte("hi")).Return(0, syscall.EIO) + nbytes, err := n.Write(context.Background(), handle, []byte("hi"), 0) + assert.Equal(t, uint32(0), nbytes) + assert.Equal(t, syscall.EIO, err) + m.AssertExpectations(t) +} +func TestNewNode(t *testing.T) { + var m mockFuseDockerClient + n := NewNode(&m, "/a", "b") + assert.Equal(t, "/a", n.fullPath) + assert.Equal(t, []byte("b"), n.Data) + assert.Equal(t, &m, n.fuseDockerClient) +} + +func TestNodeCreateMkdirAndMore(t *testing.T) { + var m mockFuseDockerClient + parent := NewNode(&m, "/dir", "") + fusefs.NewNodeFS(parent, &fusefs.Options{}) + m.On("create", mock.Anything, "/dir/f", 0, fs.FileMode(0644), mock.Anything).Run(func(args mock.Arguments) { + attr := args.Get(4).(*statAttr) + attr.FuseAttr = fuse.Attr{Ino: 1} + }).Return(fusefs.FileHandle(uintptr(1)), syscall.Errno(0)) + var out fuse.EntryOut + newInode, fh, _, errno := parent.Create(context.Background(), "f", 0, 0644, &out) + assert.NotNil(t, newInode) + assert.Equal(t, fusefs.FileHandle(uintptr(1)), fh) + assert.Equal(t, syscall.Errno(0), errno) + + m.On("mkdir", mock.Anything, "/dir/d", fs.FileMode(0755), mock.Anything).Run(func(args mock.Arguments) { + attr := args.Get(3).(*statAttr) + attr.FuseAttr = fuse.Attr{Ino: 2} + }).Return(syscall.Errno(0)) + newInode2, err := parent.Mkdir(context.Background(), "d", 0755, &out) + assert.NotNil(t, newInode2) + assert.Equal(t, syscall.Errno(0), err) + + m.On("seek", mock.Anything, fh, int64(0), 0).Return(int64(0), syscall.Errno(0)) + n, serr := parent.Lseek(context.Background(), fh, 0, 0) + assert.Equal(t, uint64(0), n) + assert.Equal(t, syscall.Errno(0), serr) +} + +func TestNodeLinkSymlinkRenameEtc(t *testing.T) { + var m mockFuseDockerClient + dir := NewNode(&m, "/dir", "") + fusefs.NewNodeFS(dir, &fusefs.Options{}) + target := NewNode(&m, "/dir/t", "") + m.On("link", mock.Anything, "/dir/t", "/dir/l").Return(syscall.Errno(0)) + m.On("stat", mock.Anything, "/dir/l", mock.Anything).Run(func(args mock.Arguments) { + attr := args.Get(2).(*statAttr) + attr.FuseAttr = fuse.Attr{Ino: 3} + }).Return(syscall.Errno(0)) + var out fuse.EntryOut + n, errno := dir.Link(context.Background(), target, "l", &out) + assert.NotNil(t, n) + assert.Equal(t, syscall.Errno(0), errno) + + m.On("symlink", mock.Anything, "/dir/t", "/dir/s").Return(syscall.Errno(0)) + n2, syErr := dir.Symlink(context.Background(), "/dir/t", "s", &out) + assert.NotNil(t, n2) + assert.Equal(t, syscall.Errno(0), syErr) + + m.On("rename", mock.Anything, "/dir/t", "r", uint32(0)).Return(syscall.Errno(0)) + rerr := dir.Rename(context.Background(), "t", dir, "r", 0) + assert.Equal(t, syscall.Errno(0), rerr) + + m.On("rmdir", mock.Anything, "/dir/x").Return(syscall.Errno(0)) + rderr := dir.Rmdir(context.Background(), "x") + assert.Equal(t, syscall.Errno(0), rderr) + + m.On("unlink", mock.Anything, "/dir/u").Return(syscall.Errno(0)) + uerr := dir.Unlink(context.Background(), "u") + assert.Equal(t, syscall.Errno(0), uerr) +} + +func TestNodeSetattrWriteSuccess(t *testing.T) { + var m mockFuseDockerClient + n := NewNode(&m, "/f", "") + m.On("setAttr", mock.Anything, "/f", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { + out := args.Get(3).(*statAttr) + out.FuseAttr = fuse.Attr{Ino: 1} + }).Return(syscall.Errno(0)) + var out fuse.AttrOut + errno := n.Setattr(context.Background(), nil, &fuse.SetAttrIn{SetAttrInCommon: fuse.SetAttrInCommon{Valid: fuse.FATTR_SIZE, Size: 1}}, &out) + assert.Equal(t, syscall.Errno(0), errno) + + handle := fusefs.FileHandle(uintptr(1)) + m.On("write", mock.Anything, handle, int64(0), []byte("hi")).Return(2, syscall.Errno(0)) + wn, werr := n.Write(context.Background(), handle, []byte("hi"), 0) + assert.Equal(t, uint32(2), wn) + assert.Equal(t, syscall.Errno(0), werr) +} + +func TestNodeLookupSuccessTypes(t *testing.T) { + tests := []struct { + name string + mode uint32 + }{ + {"dir", fuse.S_IFDIR}, + {"symlink", fuse.S_IFLNK}, + {"fifo", fuse.S_IFIFO}, + {"reg", fuse.S_IFREG}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var m mockFuseDockerClient + root := NewNode(&m, "/root", "") + fusefs.NewNodeFS(root, &fusefs.Options{}) + m.On("stat", mock.Anything, "/root/"+tt.name, mock.Anything).Run(func(args mock.Arguments) { + attr := args.Get(2).(*statAttr) + attr.FuseAttr = fuse.Attr{Ino: 1, Mode: tt.mode} + }).Return(syscall.Errno(0)) + var out fuse.EntryOut + inode, errno := root.Lookup(context.Background(), tt.name, &out) + assert.Equal(t, syscall.Errno(0), errno) + if assert.NotNil(t, inode) { + assert.Equal(t, tt.mode, inode.Mode()) + } + m.AssertExpectations(t) + }) + } +} + +func TestNodeLookupError(t *testing.T) { + var m mockFuseDockerClient + root := NewNode(&m, "/root", "") + fusefs.NewNodeFS(root, &fusefs.Options{}) + m.On("stat", mock.Anything, "/root/missing", mock.Anything).Return(syscall.ENOENT) + var out fuse.EntryOut + inode, errno := root.Lookup(context.Background(), "missing", &out) + assert.Nil(t, inode) + assert.Equal(t, syscall.ENOENT, errno) + m.AssertExpectations(t) +} diff --git a/cmd/dockerfuse/client/filesystem_test.go b/cmd/dockerfuse/client/filesystem_test.go new file mode 100644 index 0000000..2700af5 --- /dev/null +++ b/cmd/dockerfuse/client/filesystem_test.go @@ -0,0 +1,31 @@ +package client + +import ( + "os" + "testing" +) + +func TestOSFSReadFile(t *testing.T) { + dir := t.TempDir() + path := dir + "/f" + if err := os.WriteFile(path, []byte("hello"), 0600); err != nil { + t.Fatalf("write: %v", err) + } + data, err := (&osFS{}).ReadFile(path) + if err != nil { + t.Fatalf("read: %v", err) + } + if string(data) != "hello" { + t.Fatalf("got %s", data) + } +} + +func TestOSFSExecutable(t *testing.T) { + exe, err := (&osFS{}).Executable() + if err != nil { + t.Fatalf("executable err: %v", err) + } + if exe == "" { + t.Fatal("empty executable") + } +} diff --git a/cmd/dockerfuse/client/rcp_client_test.go b/cmd/dockerfuse/client/rcp_client_test.go new file mode 100644 index 0000000..65e9967 --- /dev/null +++ b/cmd/dockerfuse/client/rcp_client_test.go @@ -0,0 +1,32 @@ +package client + +import ( + "net" + "net/rpc" + "testing" +) + +type echo int + +func (e *echo) Echo(in string, out *string) error { + *out = in + return nil +} + +func TestRPCClientFactoryNewClient(t *testing.T) { + serverConn, clientConn := net.Pipe() + srv := rpc.NewServer() + srv.RegisterName("Echo", new(echo)) + go srv.ServeConn(serverConn) + + c := (&rpcClientFactory{}).NewClient(clientConn) + defer c.Close() + + var reply string + if err := c.Call("Echo.Echo", "hi", &reply); err != nil { + t.Fatalf("call failed: %v", err) + } + if reply != "hi" { + t.Fatalf("unexpected reply %s", reply) + } +} diff --git a/pkg/rpccommon/rpc_types_test.go b/pkg/rpccommon/rpc_types_test.go new file mode 100644 index 0000000..a6836a7 --- /dev/null +++ b/pkg/rpccommon/rpc_types_test.go @@ -0,0 +1,58 @@ +package rpccommon + +import ( + "testing" + "time" +) + +func TestSetAttrRequestSettersGetters(t *testing.T) { + var r SetAttrRequest + tm := time.Unix(10, 0) + r.SetMode(0755) + r.SetUID(1) + r.SetGID(2) + r.SetATime(tm) + r.SetMTime(tm) + r.SetSize(64) + + if m, ok := r.GetMode(); !ok || m != 0755 { + t.Fatalf("mode") + } + if u, ok := r.GetUID(); !ok || u != 1 { + t.Fatalf("uid") + } + if g, ok := r.GetGID(); !ok || g != 2 { + t.Fatalf("gid") + } + if a, ok := r.GetATime(); !ok || !a.Equal(tm) { + t.Fatalf("atime") + } + if mtime, ok := r.GetMTime(); !ok || !mtime.Equal(tm) { + t.Fatalf("mtime") + } + if s, ok := r.GetSize(); !ok || s != 64 { + t.Fatalf("size") + } +} + +func TestSetAttrRequestUnset(t *testing.T) { + var r SetAttrRequest + if _, ok := r.GetMode(); ok { + t.Fatal("mode set") + } + if _, ok := r.GetUID(); ok { + t.Fatal("uid set") + } + if _, ok := r.GetGID(); ok { + t.Fatal("gid set") + } + if _, ok := r.GetATime(); ok { + t.Fatal("atime set") + } + if _, ok := r.GetMTime(); ok { + t.Fatal("mtime set") + } + if _, ok := r.GetSize(); ok { + t.Fatal("size set") + } +} diff --git a/pkg/rpccommon/utils_test.go b/pkg/rpccommon/utils_test.go new file mode 100644 index 0000000..f1f96d4 --- /dev/null +++ b/pkg/rpccommon/utils_test.go @@ -0,0 +1,52 @@ +package rpccommon + +import ( + "errors" + "io/fs" + "syscall" + "testing" +) + +func TestFlagsConversion(t *testing.T) { + sys := syscall.O_WRONLY | syscall.O_CREAT | syscall.O_TRUNC + sa := SystemToSAFlags(sys) + if sa == 0 { + t.Fatalf("expected non zero") + } + got := SAFlagsToSystem(sa) + if got != sys { + t.Fatalf("expected %d got %d", sys, got) + } +} + +func TestErrnoSymConversion(t *testing.T) { + if ErrnoToSym(syscall.EPERM) != "EPERM" { + t.Fatalf("unexpected sym") + } + if SymToErrno("EPERM") != syscall.EPERM { + t.Fatalf("unexpected errno") + } +} + +func TestErrnoToRPCErrorStringAndBack(t *testing.T) { + err := ErrnoToRPCErrorString(&fs.PathError{Err: syscall.EACCES}) + if err == nil || err.Error() != "errno: EACCES" { + t.Fatalf("unexpected %v", err) + } + if RPCErrorStringTOErrno(err) != syscall.EACCES { + t.Fatalf("unexpected errno") + } +} + +func TestRPCErrorStringTOErrnoMalformed(t *testing.T) { + if RPCErrorStringTOErrno(errors.New("bad")) != syscall.EIO { + t.Fatalf("expected EIO") + } +} + +func TestErrnoToRPCErrorStringEOF(t *testing.T) { + err := ErrnoToRPCErrorString(errors.New("EOF")) + if err == nil || err.Error() != "EOF" { + t.Fatalf("unexpected %v", err) + } +}