123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428 |
- //go:build !race
- package docker_test
- import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io"
- "net"
- "net/http"
- "os"
- "path/filepath"
- "runtime"
- "strings"
- "testing"
- "time"
- "github.com/rclone/rclone/cmd/mountlib"
- "github.com/rclone/rclone/cmd/serve/docker"
- "github.com/rclone/rclone/fs"
- "github.com/rclone/rclone/fs/config"
- "github.com/rclone/rclone/fstest"
- "github.com/rclone/rclone/fstest/testy"
- "github.com/rclone/rclone/lib/file"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- _ "github.com/rclone/rclone/backend/local"
- _ "github.com/rclone/rclone/backend/memory"
- _ "github.com/rclone/rclone/cmd/cmount"
- _ "github.com/rclone/rclone/cmd/mount"
- )
- func initialise(ctx context.Context, t *testing.T) (string, fs.Fs) {
- fstest.Initialise()
- // Make test cache directory
- testDir, err := fstest.LocalRemote()
- require.NoError(t, err)
- err = file.MkdirAll(testDir, 0755)
- require.NoError(t, err)
- // Make test file system
- testFs, err := fs.NewFs(ctx, testDir)
- require.NoError(t, err)
- return testDir, testFs
- }
- func assertErrorContains(t *testing.T, err error, errString string, msgAndArgs ...interface{}) {
- assert.Error(t, err)
- if err != nil {
- assert.Contains(t, err.Error(), errString, msgAndArgs...)
- }
- }
- func assertVolumeInfo(t *testing.T, v *docker.VolInfo, name, path string) {
- assert.Equal(t, name, v.Name)
- assert.Equal(t, path, v.Mountpoint)
- assert.NotEmpty(t, v.CreatedAt)
- _, err := time.Parse(time.RFC3339, v.CreatedAt)
- assert.NoError(t, err)
- }
- func TestDockerPluginLogic(t *testing.T) {
- ctx := context.Background()
- oldCacheDir := config.GetCacheDir()
- testDir, testFs := initialise(ctx, t)
- err := config.SetCacheDir(testDir)
- require.NoError(t, err)
- defer func() {
- _ = config.SetCacheDir(oldCacheDir)
- if !t.Failed() {
- fstest.Purge(testFs)
- _ = os.RemoveAll(testDir)
- }
- }()
- // Create dummy volume driver
- drv, err := docker.NewDriver(ctx, testDir, nil, nil, true, true)
- require.NoError(t, err)
- require.NotNil(t, drv)
- // 1st volume request
- volReq := &docker.CreateRequest{
- Name: "vol1",
- Options: docker.VolOpts{},
- }
- assertErrorContains(t, drv.Create(volReq), "volume must have either remote or backend")
- volReq.Options["remote"] = testDir
- assert.NoError(t, drv.Create(volReq))
- path1 := filepath.Join(testDir, "vol1")
- assert.ErrorIs(t, drv.Create(volReq), docker.ErrVolumeExists)
- getReq := &docker.GetRequest{Name: "vol1"}
- getRes, err := drv.Get(getReq)
- assert.NoError(t, err)
- require.NotNil(t, getRes)
- assertVolumeInfo(t, getRes.Volume, "vol1", path1)
- // 2nd volume request
- volReq.Name = "vol2"
- assert.NoError(t, drv.Create(volReq))
- path2 := filepath.Join(testDir, "vol2")
- listRes, err := drv.List()
- require.NoError(t, err)
- require.Equal(t, 2, len(listRes.Volumes))
- assertVolumeInfo(t, listRes.Volumes[0], "vol1", path1)
- assertVolumeInfo(t, listRes.Volumes[1], "vol2", path2)
- // Try prohibited volume options
- volReq.Name = "vol99"
- volReq.Options["remote"] = testDir
- volReq.Options["type"] = "memory"
- err = drv.Create(volReq)
- assertErrorContains(t, err, "volume must have either remote or backend")
- volReq.Options["persist"] = "WrongBoolean"
- err = drv.Create(volReq)
- assertErrorContains(t, err, "cannot parse option")
- volReq.Options["persist"] = "true"
- delete(volReq.Options, "remote")
- err = drv.Create(volReq)
- assertErrorContains(t, err, "persist remotes is prohibited")
- volReq.Options["persist"] = "false"
- volReq.Options["memory-option-broken"] = "some-value"
- err = drv.Create(volReq)
- assertErrorContains(t, err, "unsupported backend option")
- getReq.Name = "vol99"
- getRes, err = drv.Get(getReq)
- assert.Error(t, err)
- assert.Nil(t, getRes)
- // Test mount requests
- mountReq := &docker.MountRequest{
- Name: "vol2",
- ID: "id1",
- }
- mountRes, err := drv.Mount(mountReq)
- assert.NoError(t, err)
- require.NotNil(t, mountRes)
- assert.Equal(t, path2, mountRes.Mountpoint)
- mountRes, err = drv.Mount(mountReq)
- assert.Error(t, err)
- assert.Nil(t, mountRes)
- assertErrorContains(t, err, "already mounted by this id")
- mountReq.ID = "id2"
- mountRes, err = drv.Mount(mountReq)
- assert.NoError(t, err)
- require.NotNil(t, mountRes)
- assert.Equal(t, path2, mountRes.Mountpoint)
- unmountReq := &docker.UnmountRequest{
- Name: "vol2",
- ID: "id1",
- }
- err = drv.Unmount(unmountReq)
- assert.NoError(t, err)
- err = drv.Unmount(unmountReq)
- assert.Error(t, err)
- assertErrorContains(t, err, "not mounted by this id")
- // Simulate plugin restart
- drv2, err := docker.NewDriver(ctx, testDir, nil, nil, true, false)
- assert.NoError(t, err)
- require.NotNil(t, drv2)
- // New plugin instance should pick up the saved state
- listRes, err = drv2.List()
- require.NoError(t, err)
- require.Equal(t, 2, len(listRes.Volumes))
- assertVolumeInfo(t, listRes.Volumes[0], "vol1", path1)
- assertVolumeInfo(t, listRes.Volumes[1], "vol2", path2)
- rmReq := &docker.RemoveRequest{Name: "vol2"}
- err = drv.Remove(rmReq)
- assertErrorContains(t, err, "volume is in use")
- unmountReq.ID = "id1"
- err = drv.Unmount(unmountReq)
- assert.Error(t, err)
- assertErrorContains(t, err, "not mounted by this id")
- unmountReq.ID = "id2"
- err = drv.Unmount(unmountReq)
- assert.NoError(t, err)
- err = drv.Unmount(unmountReq)
- assert.EqualError(t, err, "volume is not mounted")
- err = drv.Remove(rmReq)
- assert.NoError(t, err)
- }
- const (
- httpTimeout = 2 * time.Second
- tempDelay = 10 * time.Millisecond
- )
- type APIClient struct {
- t *testing.T
- cli *http.Client
- host string
- }
- func newAPIClient(t *testing.T, host, unixPath string) *APIClient {
- tr := &http.Transport{
- MaxIdleConns: 1,
- IdleConnTimeout: httpTimeout,
- DisableCompression: true,
- }
- if unixPath != "" {
- tr.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) {
- return net.Dial("unix", unixPath)
- }
- } else {
- dialer := &net.Dialer{
- Timeout: httpTimeout,
- KeepAlive: httpTimeout,
- }
- tr.DialContext = dialer.DialContext
- }
- cli := &http.Client{
- Transport: tr,
- Timeout: httpTimeout,
- }
- return &APIClient{
- t: t,
- cli: cli,
- host: host,
- }
- }
- func (a *APIClient) request(path string, in, out interface{}, wantErr bool) {
- t := a.t
- var (
- dataIn []byte
- dataOut []byte
- err error
- )
- realm := "VolumeDriver"
- if path == "Activate" {
- realm = "Plugin"
- }
- url := fmt.Sprintf("http://%s/%s.%s", a.host, realm, path)
- if str, isString := in.(string); isString {
- dataIn = []byte(str)
- } else {
- dataIn, err = json.Marshal(in)
- require.NoError(t, err)
- }
- fs.Logf(path, "<-- %s", dataIn)
- req, err := http.NewRequest("POST", url, bytes.NewBuffer(dataIn))
- require.NoError(t, err)
- req.Header.Set("Content-Type", "application/json")
- res, err := a.cli.Do(req)
- require.NoError(t, err)
- wantStatus := http.StatusOK
- if wantErr {
- wantStatus = http.StatusInternalServerError
- }
- assert.Equal(t, wantStatus, res.StatusCode)
- dataOut, err = io.ReadAll(res.Body)
- require.NoError(t, err)
- err = res.Body.Close()
- require.NoError(t, err)
- if strPtr, isString := out.(*string); isString || wantErr {
- require.True(t, isString, "must use string for error response")
- if wantErr {
- var errRes docker.ErrorResponse
- err = json.Unmarshal(dataOut, &errRes)
- require.NoError(t, err)
- *strPtr = errRes.Err
- } else {
- *strPtr = strings.TrimSpace(string(dataOut))
- }
- } else {
- err = json.Unmarshal(dataOut, out)
- require.NoError(t, err)
- }
- fs.Logf(path, "--> %s", dataOut)
- time.Sleep(tempDelay)
- }
- func testMountAPI(t *testing.T, sockAddr string) {
- // Disable tests under macOS and linux in the CI since they are locking up
- if runtime.GOOS == "darwin" || runtime.GOOS == "linux" {
- testy.SkipUnreliable(t)
- }
- if _, mountFn := mountlib.ResolveMountMethod(""); mountFn == nil {
- t.Skip("Test requires working mount command")
- }
- ctx := context.Background()
- oldCacheDir := config.GetCacheDir()
- testDir, testFs := initialise(ctx, t)
- err := config.SetCacheDir(testDir)
- require.NoError(t, err)
- defer func() {
- _ = config.SetCacheDir(oldCacheDir)
- if !t.Failed() {
- fstest.Purge(testFs)
- _ = os.RemoveAll(testDir)
- }
- }()
- // Prepare API client
- var cli *APIClient
- var unixPath string
- if sockAddr != "" {
- cli = newAPIClient(t, sockAddr, "")
- } else {
- unixPath = filepath.Join(testDir, "rclone.sock")
- cli = newAPIClient(t, "localhost", unixPath)
- }
- // Create mounting volume driver and listen for requests
- drv, err := docker.NewDriver(ctx, testDir, nil, nil, false, true)
- require.NoError(t, err)
- require.NotNil(t, drv)
- defer drv.Exit()
- srv := docker.NewServer(drv)
- go func() {
- var errServe error
- if unixPath != "" {
- errServe = srv.ServeUnix(unixPath, os.Getgid())
- } else {
- errServe = srv.ServeTCP(sockAddr, testDir, nil, false)
- }
- assert.ErrorIs(t, errServe, http.ErrServerClosed)
- }()
- defer func() {
- err := srv.Shutdown(ctx)
- assert.NoError(t, err)
- fs.Logf(nil, "Server stopped")
- time.Sleep(tempDelay)
- }()
- time.Sleep(tempDelay) // Let server start
- // Run test sequence
- path1 := filepath.Join(testDir, "path1")
- require.NoError(t, file.MkdirAll(path1, 0755))
- mount1 := filepath.Join(testDir, "vol1")
- res := ""
- cli.request("Activate", "{}", &res, false)
- assert.Contains(t, res, `"VolumeDriver"`)
- createReq := docker.CreateRequest{
- Name: "vol1",
- Options: docker.VolOpts{"remote": path1},
- }
- cli.request("Create", createReq, &res, false)
- assert.Equal(t, "{}", res)
- cli.request("Create", createReq, &res, true)
- assert.Contains(t, res, "volume already exists")
- mountReq := docker.MountRequest{Name: "vol1", ID: "id1"}
- var mountRes docker.MountResponse
- cli.request("Mount", mountReq, &mountRes, false)
- assert.Equal(t, mount1, mountRes.Mountpoint)
- cli.request("Mount", mountReq, &res, true)
- assert.Contains(t, res, "already mounted by this id")
- removeReq := docker.RemoveRequest{Name: "vol1"}
- cli.request("Remove", removeReq, &res, true)
- assert.Contains(t, res, "volume is in use")
- text := []byte("banana")
- err = os.WriteFile(filepath.Join(mount1, "txt"), text, 0644)
- assert.NoError(t, err)
- time.Sleep(tempDelay)
- text2, err := os.ReadFile(filepath.Join(path1, "txt"))
- assert.NoError(t, err)
- if runtime.GOOS != "windows" {
- // this check sometimes fails on windows - ignore
- assert.Equal(t, text, text2)
- }
- unmountReq := docker.UnmountRequest{Name: "vol1", ID: "id1"}
- cli.request("Unmount", unmountReq, &res, false)
- assert.Equal(t, "{}", res)
- cli.request("Unmount", unmountReq, &res, true)
- assert.Equal(t, "volume is not mounted", res)
- cli.request("Remove", removeReq, &res, false)
- assert.Equal(t, "{}", res)
- cli.request("Remove", removeReq, &res, true)
- assert.Equal(t, "volume not found", res)
- var listRes docker.ListResponse
- cli.request("List", "{}", &listRes, false)
- assert.Empty(t, listRes.Volumes)
- }
- func TestDockerPluginMountTCP(t *testing.T) {
- testMountAPI(t, "localhost:53789")
- }
- func TestDockerPluginMountUnix(t *testing.T) {
- if runtime.GOOS != "linux" {
- t.Skip("Test is Linux-only")
- }
- testMountAPI(t, "")
- }
|