github.com/rish1988/moby@v25.0.2+incompatible/integration/volume/mount_test.go (about)

     1  package volume
     2  
     3  import (
     4  	"context"
     5  	"path/filepath"
     6  	"strings"
     7  	"testing"
     8  
     9  	containertypes "github.com/docker/docker/api/types/container"
    10  	"github.com/docker/docker/api/types/mount"
    11  	"github.com/docker/docker/api/types/network"
    12  	"github.com/docker/docker/api/types/versions"
    13  	"github.com/docker/docker/api/types/volume"
    14  	"github.com/docker/docker/client"
    15  	"github.com/docker/docker/integration/internal/container"
    16  	"github.com/docker/docker/internal/safepath"
    17  	"gotest.tools/v3/assert"
    18  	is "gotest.tools/v3/assert/cmp"
    19  	"gotest.tools/v3/skip"
    20  )
    21  
    22  func TestRunMountVolumeSubdir(t *testing.T) {
    23  	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.45"), "skip test from new feature")
    24  
    25  	ctx := setupTest(t)
    26  	apiClient := testEnv.APIClient()
    27  
    28  	testVolumeName := setupTestVolume(t, apiClient)
    29  
    30  	for _, tc := range []struct {
    31  		name         string
    32  		opts         mount.VolumeOptions
    33  		cmd          []string
    34  		volumeTarget string
    35  		createErr    string
    36  		startErr     string
    37  		expected     string
    38  		skipPlatform string
    39  	}{
    40  		{name: "subdir", opts: mount.VolumeOptions{Subpath: "subdir"}, cmd: []string{"ls", "/volume"}, expected: "hello.txt"},
    41  		{name: "subdir link", opts: mount.VolumeOptions{Subpath: "hack/good"}, cmd: []string{"ls", "/volume"}, expected: "hello.txt"},
    42  		{name: "subdir with copy data", opts: mount.VolumeOptions{Subpath: "bin"}, volumeTarget: "/bin", cmd: []string{"ls", "/bin/busybox"}, expected: "/bin/busybox", skipPlatform: "windows:copy not supported on Windows"},
    43  		{name: "file", opts: mount.VolumeOptions{Subpath: "bar.txt"}, cmd: []string{"cat", "/volume"}, expected: "foo", skipPlatform: "windows:file bind mounts not supported on Windows"},
    44  		{name: "relative with backtracks", opts: mount.VolumeOptions{Subpath: "../../../../../../etc/passwd"}, cmd: []string{"cat", "/volume"}, createErr: "subpath must be a relative path within the volume"},
    45  		{name: "not existing", opts: mount.VolumeOptions{Subpath: "not-existing-path"}, cmd: []string{"cat", "/volume"}, startErr: (&safepath.ErrNotAccessible{}).Error()},
    46  
    47  		{name: "mount link", opts: mount.VolumeOptions{Subpath: filepath.Join("hack", "root")}, cmd: []string{"ls", "/volume"}, startErr: (&safepath.ErrEscapesBase{}).Error()},
    48  		{name: "mount link link", opts: mount.VolumeOptions{Subpath: filepath.Join("hack", "bad")}, cmd: []string{"ls", "/volume"}, startErr: (&safepath.ErrEscapesBase{}).Error()},
    49  	} {
    50  		t.Run(tc.name, func(t *testing.T) {
    51  			if tc.skipPlatform != "" {
    52  				platform, reason, _ := strings.Cut(tc.skipPlatform, ":")
    53  				if testEnv.DaemonInfo.OSType == platform {
    54  					t.Skip(reason)
    55  				}
    56  			}
    57  
    58  			cfg := containertypes.Config{
    59  				Image: "busybox",
    60  				Cmd:   tc.cmd,
    61  			}
    62  			hostCfg := containertypes.HostConfig{
    63  				Mounts: []mount.Mount{
    64  					{
    65  						Type:          mount.TypeVolume,
    66  						Source:        testVolumeName,
    67  						Target:        "/volume",
    68  						VolumeOptions: &tc.opts,
    69  					},
    70  				},
    71  			}
    72  			if testEnv.DaemonInfo.OSType == "windows" {
    73  				hostCfg.Mounts[0].Target = `C:\volume`
    74  			}
    75  			if tc.volumeTarget != "" {
    76  				hostCfg.Mounts[0].Target = tc.volumeTarget
    77  			}
    78  
    79  			ctrName := strings.ReplaceAll(t.Name(), "/", "_")
    80  			create, creatErr := apiClient.ContainerCreate(ctx, &cfg, &hostCfg, &network.NetworkingConfig{}, nil, ctrName)
    81  			id := create.ID
    82  			if id != "" {
    83  				defer apiClient.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true})
    84  			}
    85  
    86  			if tc.createErr != "" {
    87  				assert.ErrorContains(t, creatErr, tc.createErr)
    88  				return
    89  			}
    90  			assert.NilError(t, creatErr, "container creation failed")
    91  
    92  			startErr := apiClient.ContainerStart(ctx, id, containertypes.StartOptions{})
    93  			if tc.startErr != "" {
    94  				assert.ErrorContains(t, startErr, tc.startErr)
    95  				return
    96  			}
    97  			assert.NilError(t, startErr)
    98  
    99  			output, err := container.Output(ctx, apiClient, id)
   100  			assert.Check(t, err)
   101  			t.Logf("stdout:\n%s", output.Stdout)
   102  			t.Logf("stderr:\n%s", output.Stderr)
   103  
   104  			inspect, err := apiClient.ContainerInspect(ctx, id)
   105  			if assert.Check(t, err) {
   106  				assert.Check(t, is.Equal(inspect.State.ExitCode, 0))
   107  			}
   108  
   109  			assert.Check(t, is.Equal(strings.TrimSpace(output.Stderr), ""))
   110  			assert.Check(t, is.Equal(strings.TrimSpace(output.Stdout), tc.expected))
   111  		})
   112  	}
   113  }
   114  
   115  // setupTestVolume sets up a volume with:
   116  // .
   117  // |-- bar.txt                        (file with "foo")
   118  // |-- bin                            (directory)
   119  // |-- subdir                         (directory)
   120  // |   |-- hello.txt                  (file with "world")
   121  // |-- hack                           (directory)
   122  // |   |-- root                       (symlink to /)
   123  // |   |-- good                       (symlink to ../subdir)
   124  // |   |-- bad                        (symlink to root)
   125  func setupTestVolume(t *testing.T, client client.APIClient) string {
   126  	t.Helper()
   127  	ctx := context.Background()
   128  
   129  	volumeName := t.Name() + "-volume"
   130  
   131  	err := client.VolumeRemove(ctx, volumeName, true)
   132  	assert.NilError(t, err, "failed to clean volume")
   133  
   134  	_, err = client.VolumeCreate(ctx, volume.CreateOptions{
   135  		Name: volumeName,
   136  	})
   137  	assert.NilError(t, err, "failed to setup volume")
   138  
   139  	mount := mount.Mount{
   140  		Type:   mount.TypeVolume,
   141  		Source: volumeName,
   142  		Target: "/volume",
   143  	}
   144  
   145  	rootFs := "/"
   146  	if testEnv.DaemonInfo.OSType == "windows" {
   147  		mount.Target = `C:\volume`
   148  		rootFs = `C:`
   149  	}
   150  
   151  	initCmd := "echo foo > /volume/bar.txt && " +
   152  		"mkdir /volume/bin && " +
   153  		"mkdir /volume/subdir && " +
   154  		"echo world > /volume/subdir/hello.txt && " +
   155  		"mkdir /volume/hack && " +
   156  		"ln -s " + rootFs + " /volume/hack/root && " +
   157  		"ln -s ../subdir /volume/hack/good && " +
   158  		"ln -s root /volume/hack/bad &&" +
   159  		"mkdir /volume/hack/iwanttobehackedwithtoctou"
   160  
   161  	opts := []func(*container.TestContainerConfig){
   162  		container.WithMount(mount),
   163  		container.WithCmd("sh", "-c", initCmd+"; ls -lah /volume /volume/hack/"),
   164  	}
   165  	if testEnv.DaemonInfo.OSType == "windows" {
   166  		// Can't create symlinks under HyperV isolation
   167  		opts = append(opts, container.WithIsolation(containertypes.IsolationProcess))
   168  	}
   169  
   170  	cid := container.Run(ctx, t, client, opts...)
   171  	defer client.ContainerRemove(ctx, cid, containertypes.RemoveOptions{Force: true})
   172  	output, err := container.Output(ctx, client, cid)
   173  
   174  	t.Logf("Setup stderr:\n%s", output.Stderr)
   175  	t.Logf("Setup stdout:\n%s", output.Stdout)
   176  
   177  	assert.NilError(t, err)
   178  	assert.Assert(t, is.Equal(output.Stderr, ""))
   179  
   180  	inspect, err := client.ContainerInspect(ctx, cid)
   181  	assert.NilError(t, err)
   182  	assert.Assert(t, is.Equal(inspect.State.ExitCode, 0))
   183  
   184  	return volumeName
   185  }