Writing to the mount with os.WriteFile made the Go runtime register
the file with its poller so the kernel then polled the file from
epoll_ctl and epoll_wait, sending POLL requests to the FUSE server
running in this same process. A thread waiting inside epoll cannot be
preempted by the runtime, so a garbage collection starting while such
a POLL was outstanding stopped the world for good - the test binary
could not be killed even with SIGKILL and the mount was left behind,
wedging anything that touched it.
Now we write through the mount with a descriptor straight from
open(2), which os.NewFile keeps out of the poller, check that it
really is out of the poller with SetDeadline, and check at the end of
the test that the mountpoint is unmounted.
In this commit we fixed the same problem for mount by running in a
subprocess however changing one write file routine here was much
easier than re-arranging the tests.
4a382c09ec mount: run tests in a subprocess to fix deadlock - #3259
Note that go-fuse (and hence mount2) works around this problem it by
forcing an early POLL it can answer with ENOSYS.
See: https://github.com/golang/go/issues/21014
643 lines
18 KiB
Go
643 lines
18 KiB
Go
//go:build !race
|
|
|
|
package docker_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
"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 ...any) {
|
|
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")
|
|
|
|
// Create is idempotent - creating the same volume again should succeed
|
|
assert.NoError(t, drv.Create(volReq))
|
|
|
|
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)
|
|
}
|
|
|
|
// TestDockerPluginVolumeNameConfined checks that a crafted volume name
|
|
// containing ".." components cannot escape the base directory to create a
|
|
// mountpoint at an arbitrary host path.
|
|
func TestDockerPluginVolumeNameConfined(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)
|
|
}
|
|
}()
|
|
|
|
drv, err := docker.NewDriver(ctx, testDir, nil, nil, true, true)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, drv)
|
|
|
|
// A volume name with ".." components resolves outside testDir, to a
|
|
// sibling path that does not exist so the test never touches a real
|
|
// system directory.
|
|
escape := filepath.Join("..", "docker-escape-target")
|
|
outside := filepath.Join(filepath.Dir(testDir), "docker-escape-target")
|
|
require.NoDirExists(t, outside)
|
|
volReq := &docker.CreateRequest{
|
|
Name: escape,
|
|
Options: docker.VolOpts{"remote": testDir},
|
|
}
|
|
err = drv.Create(volReq)
|
|
assertErrorContains(t, err, "resolves outside the base directory")
|
|
|
|
// The escaping directory must not have been created.
|
|
_, statErr := os.Stat(outside)
|
|
assert.True(t, os.IsNotExist(statErr), "escaping mountpoint %q should not exist", outside)
|
|
|
|
// The volume must not have been registered.
|
|
_, err = drv.Get(&docker.GetRequest{Name: escape})
|
|
assert.Error(t, err)
|
|
|
|
// A name that resolves to the base directory itself (empty or ".")
|
|
// must also be rejected, since mounting there would shadow every
|
|
// other volume.
|
|
for _, name := range []string{"", ".", filepath.Join("a", "..")} {
|
|
volReq.Name = name
|
|
err = drv.Create(volReq)
|
|
assertErrorContains(t, err, "resolves outside the base directory",
|
|
"name %q should be rejected", name)
|
|
}
|
|
}
|
|
|
|
// TestDockerPluginRestoreStateConfined checks that a persisted state file
|
|
// with a mountpoint escaping the base directory is not trusted verbatim on
|
|
// restore: the mountpoint is re-derived from the base directory and name.
|
|
func TestDockerPluginRestoreStateConfined(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)
|
|
}
|
|
}()
|
|
|
|
// Persist a state file as an older, vulnerable rclone might have: a
|
|
// benign name but a mountpoint that escapes the base directory.
|
|
escaped := filepath.Join(filepath.Dir(testDir), "docker-escape-restore")
|
|
require.NoDirExists(t, escaped)
|
|
state := fmt.Sprintf(`[{"name":"vol1","mountpoint":%q,"created":%q,"fs":%q,"options":{"remote":%q},"mounts":[]}]`,
|
|
escaped, time.Now().Format(time.RFC3339), testDir, testDir)
|
|
statePath := filepath.Join(testDir, "docker-plugin.state")
|
|
require.NoError(t, os.WriteFile(statePath, []byte(state), 0600))
|
|
|
|
// Restore the state into a new dummy driver.
|
|
drv, err := docker.NewDriver(ctx, testDir, nil, nil, true, false)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, drv)
|
|
|
|
// The restored volume must point back inside the base directory, and
|
|
// the escaping directory must not have been created.
|
|
getRes, err := drv.Get(&docker.GetRequest{Name: "vol1"})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, getRes)
|
|
assert.Equal(t, filepath.Join(testDir, "vol1"), getRes.Volume.Mountpoint)
|
|
assert.NotEqual(t, escaped, getRes.Volume.Mountpoint)
|
|
_, statErr := os.Stat(escaped)
|
|
assert.True(t, os.IsNotExist(statErr), "escaping mountpoint %q should not exist", escaped)
|
|
}
|
|
|
|
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 any, 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)
|
|
}
|
|
|
|
// writeMountedFile writes data to a file on a FUSE mount served by this
|
|
// process.
|
|
//
|
|
// It avoids os.WriteFile as files opened with os.OpenFile are handed
|
|
// to the Go runtime poller and the kernel then asks the FUSE server
|
|
// to poll them from epoll_ctl and epoll_wait. The server lives in
|
|
// this process and a thread waiting inside epoll cannot be preempted,
|
|
// so a garbage collection starting while a poll is outstanding stops
|
|
// the world for good. The poll is never answered, the process cannot
|
|
// be killed even with SIGKILL and the mount is left behind.
|
|
// See: https://github.com/golang/go/issues/21014
|
|
//
|
|
// os.NewFile only returns a pollable file for a descriptor which is
|
|
// already in non-blocking mode, so a descriptor straight from open(2)
|
|
// stays out of the poller.
|
|
func writeMountedFile(path string, data []byte, perm uint32) (err error) {
|
|
fd, err := syscall.Open(path, syscall.O_WRONLY|syscall.O_CREAT|syscall.O_TRUNC, perm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f := os.NewFile(uintptr(fd), path)
|
|
defer fs.CheckClose(f, &err)
|
|
// Deadlines work on polled files only
|
|
if err := f.SetDeadline(time.Time{}); !errors.Is(err, os.ErrNoDeadline) {
|
|
return fmt.Errorf("%s is being polled by the runtime which risks deadlocking the test: %w", path, err)
|
|
}
|
|
_, err = f.Write(data)
|
|
return err
|
|
}
|
|
|
|
// checkUnmounted fails the test and tears the mount down if mountpoint
|
|
// is still mounted.
|
|
func checkUnmounted(t *testing.T, mountpoint string) {
|
|
// CheckMountReady and fusermount are both Linux only
|
|
if !mountlib.CanCheckMountReady || mountlib.CheckMountReady(mountpoint) != nil {
|
|
return
|
|
}
|
|
assert.Fail(t, fmt.Sprintf("mountpoint %s was left mounted", mountpoint))
|
|
// Lazy unmount as the mount may still be busy
|
|
if out, err := exec.Command("fusermount", "-uz", mountpoint).CombinedOutput(); err != nil {
|
|
t.Logf("Failed to unmount %s: %v: %s", mountpoint, err, out)
|
|
}
|
|
}
|
|
|
|
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)
|
|
mount1 := filepath.Join(testDir, "vol1")
|
|
defer func() {
|
|
_ = config.SetCacheDir(oldCacheDir)
|
|
if !t.Failed() {
|
|
fstest.Purge(testFs)
|
|
_ = os.RemoveAll(testDir)
|
|
}
|
|
}()
|
|
defer checkUnmounted(t, mount1)
|
|
|
|
// 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))
|
|
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)
|
|
// Create is idempotent - creating the same volume again should succeed
|
|
cli.request("Create", createReq, &res, false)
|
|
assert.Equal(t, "{}", res)
|
|
|
|
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 = writeMountedFile(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 TestDockerPluginDeferredMounts(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 with volumes and mounts
|
|
drv, err := docker.NewDriver(ctx, testDir, nil, nil, true, true)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, drv)
|
|
defer drv.Exit()
|
|
|
|
volReq := &docker.CreateRequest{
|
|
Name: "vol1",
|
|
Options: docker.VolOpts{"remote": testDir},
|
|
}
|
|
assert.NoError(t, drv.Create(volReq))
|
|
volReq.Name = "vol2"
|
|
assert.NoError(t, drv.Create(volReq))
|
|
|
|
// Mount vol2 with two IDs
|
|
mountReq := &docker.MountRequest{Name: "vol2", ID: "id1"}
|
|
_, err = drv.Mount(mountReq)
|
|
assert.NoError(t, err)
|
|
mountReq.ID = "id2"
|
|
_, err = drv.Mount(mountReq)
|
|
assert.NoError(t, err)
|
|
|
|
// Simulate plugin restart - state is restored but mounts are deferred
|
|
// (Don't call drv.Exit() since that clears mounts from state, simulating a crash)
|
|
drv2, err := docker.NewDriver(ctx, testDir, nil, nil, true, false)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, drv2)
|
|
defer drv2.Exit()
|
|
|
|
// Volumes should be listed (metadata restored)
|
|
listRes, err := drv2.List()
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, len(listRes.Volumes))
|
|
|
|
// vol2 should have no active mounts yet (deferred)
|
|
path2 := filepath.Join(testDir, "vol2")
|
|
assertVolumeInfo(t, listRes.Volumes[1], "vol2", path2)
|
|
|
|
// Now restore mounts
|
|
drv2.RestoreMounts()
|
|
|
|
// After RestoreMounts, vol2 should have its mounts back
|
|
listRes, err = drv2.List()
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, len(listRes.Volumes))
|
|
status := listRes.Volumes[1].Status
|
|
require.NotNil(t, status)
|
|
mounts, ok := status["Mounts"]
|
|
require.True(t, ok)
|
|
mountList, ok := mounts.([]string)
|
|
require.True(t, ok)
|
|
assert.Equal(t, 2, len(mountList))
|
|
assert.Contains(t, mountList, "id1")
|
|
assert.Contains(t, mountList, "id2")
|
|
}
|
|
|
|
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, "")
|
|
}
|