package container

import (
	"fmt"
	"os"
	"path/filepath"
	"syscall"
	"testing"

	cerrdefs "github.com/containerd/errdefs"
	containertypes "github.com/moby/moby/api/types/container"
	mounttypes "github.com/moby/moby/api/types/mount"
	"github.com/moby/moby/api/types/network"
	"github.com/moby/moby/client"
	"github.com/moby/moby/client/pkg/stringid"
	"github.com/moby/moby/client/pkg/versions"
	"github.com/moby/moby/v2/daemon/config"
	"github.com/moby/moby/v2/daemon/volume"
	"github.com/moby/moby/v2/integration/internal/container"
	"github.com/moby/moby/v2/internal/testutil"
	"github.com/moby/moby/v2/pkg/parsers/kernel"
	"github.com/moby/sys/mount"
	"github.com/moby/sys/mountinfo"
	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
	"gotest.tools/v3/fs"
	"gotest.tools/v3/poll"
	"gotest.tools/v3/skip"
)

// testNonExistingPlugin is a special plugin-name, which overrides defaultTimeOut in tests.
// this is a copy of https://github.com/moby/moby/blob/9e00a63d65434cdedc444e79a2b33a7c202b10d8/pkg/plugins/client.go#L253-L254
const testNonExistingPlugin = "this-plugin-does-not-exist"

// TestContainerMultipleMountsSamePath verifies that when a host path has
// multiple bind mount entries in /proc/self/mountinfo with different
// propagation (e.g., slave then shared), Docker reads the most recent
// entry and allows containers to use the path with shared propagation.
//
// Regression test for https://github.com/moby/moby/issues/43714
func TestContainerMultipleMountsSamePath(t *testing.T) {
	skip.If(t, testEnv.IsRemoteDaemon)
	skip.If(t, testEnv.IsUserNamespace)
	skip.If(t, testEnv.IsRootless, "cannot be tested because RootlessKit executes the daemon in private mount namespace (https://github.com/rootless-containers/rootlesskit/issues/97)")

	ctx := setupTest(t)

	tmpDir := fs.NewDir(t, "propagation-test", fs.WithMode(0o755))
	src := tmpDir.Path()

	// First bind mount + make slave → mountinfo entry 1 (master:N)
	assert.NilError(t, syscall.Mount(src, src, "", syscall.MS_BIND, ""))
	t.Cleanup(func() { syscall.Unmount(src, syscall.MNT_DETACH) })
	assert.NilError(t, syscall.Mount("", src, "", syscall.MS_SLAVE, ""))

	// Second bind mount + make shared → mountinfo entry 2 (shared:N)
	assert.NilError(t, syscall.Mount(src, src, "", syscall.MS_BIND, ""))
	t.Cleanup(func() { syscall.Unmount(src, syscall.MNT_DETACH) })
	assert.NilError(t, syscall.Mount("", src, "", syscall.MS_SHARED, ""))

	apiClient := testEnv.APIClient()
	ctrID := container.Run(ctx, t, apiClient,
		container.WithCmd("true"),
		container.WithMount(mounttypes.Mount{
			Type:   mounttypes.TypeBind,
			Source: src,
			Target: "/volume-dest",
			BindOptions: &mounttypes.BindOptions{
				Propagation: mounttypes.PropagationShared,
			},
		}),
	)
	poll.WaitOn(t, container.IsSuccessful(ctx, apiClient, ctrID))
}
func TestContainerNetworkMountsNoChown(t *testing.T) {
	// chown only applies to Linux bind mounted volumes; must be same host to verify
	skip.If(t, testEnv.IsRemoteDaemon)

	ctx := setupTest(t)

	tmpDir := fs.NewDir(t, "network-file-mounts", fs.WithMode(0o755), fs.WithFile("nwfile", "network file bind mount", fs.WithMode(0o644)))
	tmpNWFileMount := tmpDir.Join("nwfile")

	config := containertypes.Config{
		Image: "busybox",
	}
	hostConfig := containertypes.HostConfig{
		Mounts: []mounttypes.Mount{
			{
				Type:   "bind",
				Source: tmpNWFileMount,
				Target: "/etc/resolv.conf",
			},
			{
				Type:   "bind",
				Source: tmpNWFileMount,
				Target: "/etc/hostname",
			},
			{
				Type:   "bind",
				Source: tmpNWFileMount,
				Target: "/etc/hosts",
			},
		},
	}

	cli, err := client.New(client.FromEnv)
	assert.NilError(t, err)
	defer cli.Close()

	ctrCreate, err := cli.ContainerCreate(ctx, client.ContainerCreateOptions{
		Config:           &config,
		HostConfig:       &hostConfig,
		NetworkingConfig: &network.NetworkingConfig{},
	})
	assert.NilError(t, err)
	// container will exit immediately because of no tty, but we only need the start sequence to test the condition
	_, err = cli.ContainerStart(ctx, ctrCreate.ID, client.ContainerStartOptions{})
	assert.NilError(t, err)

	// Check that host-located bind mount network file did not change ownership when the container was started
	// Note: If the user specifies a mountpath from the host, we should not be
	// attempting to chown files outside the daemon's metadata directory
	// (represented by `daemon.repository` at init time).
	// This forces users who want to use user namespaces to handle the
	// ownership needs of any external files mounted as network files
	// (/etc/resolv.conf, /etc/hosts, /etc/hostname) separately from the
	// daemon. In all other volume/bind mount situations we have taken this
	// same line--we don't chown host file content.
	// See GitHub PR 34224 for details.
	info, err := os.Stat(tmpNWFileMount)
	assert.NilError(t, err)
	fi := info.Sys().(*syscall.Stat_t)
	assert.Check(t, is.Equal(fi.Uid, uint32(0)), "bind mounted network file should not change ownership from root")
}

func assertContainerMountPoint(t *testing.T, mountPoint, expected containertypes.MountPoint) {
	t.Helper()

	if expected.Source != "" {
		assert.Check(t, is.Equal(mountPoint.Source, expected.Source))
	}
	if expected.Name != "" {
		assert.Check(t, is.Equal(mountPoint.Name, expected.Name))
	}
	if expected.Driver != "" {
		assert.Check(t, is.Equal(mountPoint.Driver, expected.Driver))
	}
	if expected.Propagation != "" {
		assert.Check(t, is.Equal(mountPoint.Propagation, expected.Propagation))
	}
	assert.Check(t, is.Equal(mountPoint.RW, expected.RW))
	assert.Check(t, is.Equal(mountPoint.Type, expected.Type))
	assert.Check(t, is.Equal(mountPoint.Mode, expected.Mode))
	assert.Check(t, is.Equal(mountPoint.Destination, expected.Destination))
}

func TestContainerBindMountInspect(t *testing.T) {
	skip.If(t, testEnv.IsRemoteDaemon)

	ctx := setupTest(t)
	apiClient := testEnv.APIClient()

	type testCase struct {
		desc     string
		spec     mounttypes.Mount
		expected containertypes.MountPoint
	}

	const destPath = "/foo"
	tmpDir1 := fs.NewDir(t, "test-mounts-api-1", fs.WithMode(0o755))
	tests := []testCase{{
		desc:     "readonly bind",
		spec:     mounttypes.Mount{Type: "bind", Source: tmpDir1.Path(), Target: destPath, ReadOnly: true},
		expected: containertypes.MountPoint{Type: "bind", RW: false, Destination: destPath, Source: tmpDir1.Path()},
	}}

	tmpDir3 := fs.NewDir(t, "test-mounts-api-3", fs.WithMode(0o755))
	if err := mount.Mount(tmpDir3.Path(), tmpDir3.Path(), "none", "bind,shared"); err == nil {
		t.Cleanup(func() {
			assert.NilError(t, mount.Unmount(tmpDir3.Path()))
		})
		tests = append(tests, testCase{
			desc:     "readonly bind shared propagation",
			spec:     mounttypes.Mount{Type: "bind", Source: tmpDir3.Path(), Target: destPath, ReadOnly: true, BindOptions: &mounttypes.BindOptions{Propagation: "shared"}},
			expected: containertypes.MountPoint{Type: "bind", RW: false, Destination: destPath, Source: tmpDir3.Path(), Propagation: "shared"},
		})
	}

	for _, tc := range tests {
		t.Run(tc.desc, func(t *testing.T) {
			ctx := testutil.StartSpan(ctx, t)
			ctr, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
				Config: &containertypes.Config{
					Image: "busybox",
					Cmd:   []string{"true"},
				},
				HostConfig:       &containertypes.HostConfig{Mounts: []mounttypes.Mount{tc.spec}},
				NetworkingConfig: &network.NetworkingConfig{},
			})
			assert.NilError(t, err)

			inspect, err := apiClient.ContainerInspect(ctx, ctr.ID, client.ContainerInspectOptions{})
			assert.NilError(t, err)
			assert.Assert(t, is.Len(inspect.Container.Mounts, 1))
			assertContainerMountPoint(t, inspect.Container.Mounts[0], tc.expected)

			_, err = apiClient.ContainerRemove(ctx, ctr.ID, client.ContainerRemoveOptions{
				RemoveVolumes: true,
				Force:         true,
			})
			assert.NilError(t, err)
		})
	}
}

func TestMountDaemonRoot(t *testing.T) {
	skip.If(t, testEnv.IsRemoteDaemon)

	ctx := setupTest(t)
	apiClient := testEnv.APIClient()
	result, err := apiClient.Info(ctx, client.InfoOptions{})
	if err != nil {
		t.Fatal(err)
	}
	info := result.Info

	for _, test := range []struct {
		desc        string
		propagation mounttypes.Propagation
		expected    mounttypes.Propagation
	}{
		{
			desc:        "default",
			propagation: "",
			expected:    mounttypes.PropagationRSlave,
		},
		{
			desc:        "private",
			propagation: mounttypes.PropagationPrivate,
		},
		{
			desc:        "rprivate",
			propagation: mounttypes.PropagationRPrivate,
		},
		{
			desc:        "slave",
			propagation: mounttypes.PropagationSlave,
		},
		{
			desc:        "rslave",
			propagation: mounttypes.PropagationRSlave,
			expected:    mounttypes.PropagationRSlave,
		},
		{
			desc:        "shared",
			propagation: mounttypes.PropagationShared,
		},
		{
			desc:        "rshared",
			propagation: mounttypes.PropagationRShared,
			expected:    mounttypes.PropagationRShared,
		},
	} {
		t.Run(test.desc, func(t *testing.T) {
			test := test
			t.Parallel()

			ctx := testutil.StartSpan(ctx, t)

			propagationSpec := fmt.Sprintf(":%s", test.propagation)
			if test.propagation == "" {
				propagationSpec = ""
			}
			bindSpecRoot := info.DockerRootDir + ":" + "/foo" + propagationSpec
			bindSpecSub := filepath.Join(info.DockerRootDir, "containers") + ":/foo" + propagationSpec

			for name, hc := range map[string]*containertypes.HostConfig{
				"bind root":    {Binds: []string{bindSpecRoot}},
				"bind subpath": {Binds: []string{bindSpecSub}},
				"mount root": {
					Mounts: []mounttypes.Mount{
						{
							Type:        mounttypes.TypeBind,
							Source:      info.DockerRootDir,
							Target:      "/foo",
							BindOptions: &mounttypes.BindOptions{Propagation: test.propagation},
						},
					},
				},
				"mount subpath": {
					Mounts: []mounttypes.Mount{
						{
							Type:        mounttypes.TypeBind,
							Source:      filepath.Join(info.DockerRootDir, "containers"),
							Target:      "/foo",
							BindOptions: &mounttypes.BindOptions{Propagation: test.propagation},
						},
					},
				},
			} {
				t.Run(name, func(t *testing.T) {
					hc := hc
					t.Parallel()

					ctx := testutil.StartSpan(ctx, t)

					c, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
						Config: &containertypes.Config{
							Image: "busybox",
							Cmd:   []string{"true"},
						},
						HostConfig: hc,
					})
					if err != nil {
						if test.expected != "" {
							t.Fatal(err)
						}
						// expected an error, so this is ok and should not continue
						return
					}
					if test.expected == "" {
						t.Fatal("expected create to fail")
					}

					defer func() {
						if _, err := apiClient.ContainerRemove(ctx, c.ID, client.ContainerRemoveOptions{Force: true}); err != nil {
							panic(err)
						}
					}()

					inspect, err := apiClient.ContainerInspect(ctx, c.ID, client.ContainerInspectOptions{})
					if err != nil {
						t.Fatal(err)
					}
					if len(inspect.Container.Mounts) != 1 {
						t.Fatalf("unexpected number of mounts: %+v", inspect.Container.Mounts)
					}

					m := inspect.Container.Mounts[0]
					if m.Propagation != test.expected {
						t.Fatalf("got unexpected propagation mode, expected %q, got: %v", test.expected, m.Propagation)
					}
				})
			}
		})
	}
}

func TestContainerBindMountNonRecursive(t *testing.T) {
	skip.If(t, testEnv.IsRemoteDaemon)
	skip.If(t, testEnv.IsRootless, "cannot be tested because RootlessKit executes the daemon in private mount namespace (https://github.com/rootless-containers/rootlesskit/issues/97)")

	ctx := setupTest(t)

	tmpDir1 := fs.NewDir(t, "tmpdir1", fs.WithMode(0o755), fs.WithDir("mnt", fs.WithMode(0o755)))
	tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt")
	tmpDir2 := fs.NewDir(t, "tmpdir2", fs.WithMode(0o755), fs.WithFile("file", "should not be visible when NonRecursive", fs.WithMode(0o644)))

	err := mount.Mount(tmpDir2.Path(), tmpDir1Mnt, "none", "bind,ro")
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		if err := mount.Unmount(tmpDir1Mnt); err != nil {
			t.Fatal(err)
		}
	}()

	// implicit is recursive (NonRecursive: false)
	implicit := mounttypes.Mount{
		Type:     "bind",
		Source:   tmpDir1.Path(),
		Target:   "/foo",
		ReadOnly: true,
	}
	recursive := implicit
	recursive.BindOptions = &mounttypes.BindOptions{
		NonRecursive: false,
	}
	recursiveVerifier := []string{"test", "-f", "/foo/mnt/file"}
	nonRecursive := implicit
	nonRecursive.BindOptions = &mounttypes.BindOptions{
		NonRecursive: true,
	}
	nonRecursiveVerifier := []string{"test", "!", "-f", "/foo/mnt/file"}

	apiClient := testEnv.APIClient()
	containers := []string{
		container.Run(ctx, t, apiClient, container.WithMount(implicit), container.WithCmd(recursiveVerifier...)),
		container.Run(ctx, t, apiClient, container.WithMount(recursive), container.WithCmd(recursiveVerifier...)),
		container.Run(ctx, t, apiClient, container.WithMount(nonRecursive), container.WithCmd(nonRecursiveVerifier...)),
	}

	for _, c := range containers {
		poll.WaitOn(t, container.IsSuccessful(ctx, apiClient, c))
	}
}

func TestContainerVolumesMountedAsShared(t *testing.T) {
	// Volume propagation is linux only. Also it creates directories for
	// bind mounting, so needs to be same host.
	skip.If(t, testEnv.IsRemoteDaemon)
	skip.If(t, testEnv.IsUserNamespace)
	skip.If(t, testEnv.IsRootless, "cannot be tested because RootlessKit executes the daemon in private mount namespace (https://github.com/rootless-containers/rootlesskit/issues/97)")

	ctx := setupTest(t)

	// Prepare a source directory to bind mount
	tmpDir1 := fs.NewDir(t, "volume-source", fs.WithMode(0o755), fs.WithDir("mnt1", fs.WithMode(0o755)))
	tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt1")

	// Convert this directory into a shared mount point so that we do
	// not rely on propagation properties of parent mount.
	if err := mount.MakePrivate(tmpDir1.Path()); err != nil {
		t.Fatal(err)
	}
	defer func() {
		if err := mount.Unmount(tmpDir1.Path()); err != nil {
			t.Fatal(err)
		}
	}()
	if err := mount.MakeShared(tmpDir1.Path()); err != nil {
		t.Fatal(err)
	}

	sharedMount := mounttypes.Mount{
		Type:   mounttypes.TypeBind,
		Source: tmpDir1.Path(),
		Target: "/volume-dest",
		BindOptions: &mounttypes.BindOptions{
			Propagation: mounttypes.PropagationShared,
		},
	}

	bindMountCmd := []string{"mount", "--bind", "/volume-dest/mnt1", "/volume-dest/mnt1"}

	apiClient := testEnv.APIClient()
	containerID := container.Run(ctx, t, apiClient, container.WithPrivileged(true), container.WithMount(sharedMount), container.WithCmd(bindMountCmd...))
	poll.WaitOn(t, container.IsSuccessful(ctx, apiClient, containerID))

	// Make sure a bind mount under a shared volume propagated to host.
	if mounted, _ := mountinfo.Mounted(tmpDir1Mnt); !mounted {
		t.Fatalf("Bind mount under shared volume did not propagate to host")
	}

	mount.Unmount(tmpDir1Mnt)
}

func TestContainerVolumesMountedAsSlave(t *testing.T) {
	// Volume propagation is linux only. Also it creates directories for
	// bind mounting, so needs to be same host.
	skip.If(t, testEnv.IsRemoteDaemon)
	skip.If(t, testEnv.IsUserNamespace)
	skip.If(t, testEnv.IsRootless, "cannot be tested because RootlessKit executes the daemon in private mount namespace (https://github.com/rootless-containers/rootlesskit/issues/97)")

	ctx := testutil.StartSpan(baseContext, t)

	// Prepare a source directory to bind mount
	tmpDir1 := fs.NewDir(t, "volume-source", fs.WithMode(0o755), fs.WithDir("mnt1", fs.WithMode(0o755)))
	tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt1")

	// Prepare a source directory with file in it. We will bind mount this
	// directory and see if file shows up.
	tmpDir2 := fs.NewDir(t, "volume-source2", fs.WithMode(0o755), fs.WithFile("slave-testfile", "Test", fs.WithMode(0o644)))

	// Convert this directory into a shared mount point so that we do
	// not rely on propagation properties of parent mount.
	if err := mount.MakePrivate(tmpDir1.Path()); err != nil {
		t.Fatal(err)
	}
	defer func() {
		if err := mount.Unmount(tmpDir1.Path()); err != nil {
			t.Fatal(err)
		}
	}()
	if err := mount.MakeShared(tmpDir1.Path()); err != nil {
		t.Fatal(err)
	}

	slaveMount := mounttypes.Mount{
		Type:   mounttypes.TypeBind,
		Source: tmpDir1.Path(),
		Target: "/volume-dest",
		BindOptions: &mounttypes.BindOptions{
			Propagation: mounttypes.PropagationSlave,
		},
	}

	topCmd := []string{"top"}

	apiClient := testEnv.APIClient()
	containerID := container.Run(ctx, t, apiClient, container.WithTty(true), container.WithMount(slaveMount), container.WithCmd(topCmd...), container.WithSecurityOpt("label=disable"))

	// Bind mount tmpDir2/ onto tmpDir1/mnt1. If mount propagates inside
	// container then contents of tmpDir2/slave-testfile should become
	// visible at "/volume-dest/mnt1/slave-testfile"
	if err := mount.Mount(tmpDir2.Path(), tmpDir1Mnt, "none", "bind"); err != nil {
		t.Fatal(err)
	}
	defer func() {
		if err := mount.Unmount(tmpDir1Mnt); err != nil {
			t.Fatal(err)
		}
	}()

	mountCmd := []string{"cat", "/volume-dest/mnt1/slave-testfile"}

	if result, err := container.Exec(ctx, apiClient, containerID, mountCmd); err == nil {
		if result.Stdout() != "Test" {
			t.Fatalf("Bind mount under slave volume did not propagate to container")
		}
	} else {
		t.Fatal(err)
	}
}

// TestContainerVolumeAnonymous verifies that anonymous volumes created through
// the Mounts API get a random name generated, and have the "AnonymousLabel"
// (com.docker.volume.anonymous) label set.
//
// regression test for https://github.com/moby/moby/issues/48748
func TestContainerVolumeAnonymous(t *testing.T) {
	skip.If(t, testEnv.IsRemoteDaemon)

	ctx := setupTest(t)
	apiClient := testEnv.APIClient()

	t.Run("no driver specified", func(t *testing.T) {
		mntOpts := mounttypes.Mount{Type: mounttypes.TypeVolume, Target: "/foo"}
		cID := container.Create(ctx, t, apiClient, container.WithMount(mntOpts))

		inspect := container.Inspect(ctx, t, apiClient, cID)
		assert.Assert(t, is.Len(inspect.HostConfig.Mounts, 1))
		assert.Check(t, is.Equal(inspect.HostConfig.Mounts[0], mntOpts))

		assert.Assert(t, is.Len(inspect.Mounts, 1))
		vol := inspect.Mounts[0]
		assert.Check(t, is.Len(vol.Name, 64), "volume name should be 64 bytes (from stringid.GenerateRandomID())")
		assert.Check(t, is.Equal(vol.Driver, volume.DefaultDriverName))

		res, err := apiClient.VolumeInspect(ctx, vol.Name, client.VolumeInspectOptions{})
		assert.NilError(t, err)

		// see [daemon.AnonymousLabel]; we don't want to import the daemon package here.
		const expectedAnonymousLabel = "com.docker.volume.anonymous"
		assert.Check(t, is.Contains(res.Volume.Labels, expectedAnonymousLabel))
		assert.Check(t, is.Equal(res.Volume.Driver, volume.DefaultDriverName))
	})

	t.Run("inspect mount point fields", func(t *testing.T) {
		const destPath = "/foo"
		selinuxSharedLabel := "z"

		tests := []struct {
			desc     string
			spec     mounttypes.Mount
			expected containertypes.MountPoint
		}{
			{
				desc:     "anonymous volume with trailing slash",
				spec:     mounttypes.Mount{Type: mounttypes.TypeVolume, Target: destPath + "/"},
				expected: containertypes.MountPoint{Driver: volume.DefaultDriverName, Type: mounttypes.TypeVolume, RW: true, Destination: destPath, Mode: selinuxSharedLabel},
			},
			{
				desc:     "readonly named volume",
				spec:     mounttypes.Mount{Type: mounttypes.TypeVolume, Target: destPath, ReadOnly: true, Source: stringid.GenerateRandomID()},
				expected: containertypes.MountPoint{Type: mounttypes.TypeVolume, RW: false, Destination: destPath, Mode: selinuxSharedLabel},
			},
			{
				desc:     "named volume with driver",
				spec:     mounttypes.Mount{Type: mounttypes.TypeVolume, Target: destPath, Source: stringid.GenerateRandomID(), VolumeOptions: &mounttypes.VolumeOptions{DriverConfig: &mounttypes.Driver{Name: volume.DefaultDriverName}}},
				expected: containertypes.MountPoint{Driver: volume.DefaultDriverName, Type: mounttypes.TypeVolume, RW: true, Destination: destPath, Mode: selinuxSharedLabel},
			},
			{
				desc:     "anonymous volume no copy",
				spec:     mounttypes.Mount{Type: mounttypes.TypeVolume, Target: destPath, VolumeOptions: &mounttypes.VolumeOptions{NoCopy: true}},
				expected: containertypes.MountPoint{Driver: volume.DefaultDriverName, Type: mounttypes.TypeVolume, RW: true, Destination: destPath, Mode: selinuxSharedLabel},
			},
		}

		for _, tc := range tests {
			t.Run(tc.desc, func(t *testing.T) {
				ctx := testutil.StartSpan(ctx, t)
				ctr, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
					Config: &containertypes.Config{
						Image: "busybox",
						Cmd:   []string{"true"},
					},
					HostConfig:       &containertypes.HostConfig{Mounts: []mounttypes.Mount{tc.spec}},
					NetworkingConfig: &network.NetworkingConfig{},
				})
				assert.NilError(t, err)

				inspect, err := apiClient.ContainerInspect(ctx, ctr.ID, client.ContainerInspectOptions{})
				assert.NilError(t, err)
				assert.Assert(t, is.Len(inspect.Container.Mounts, 1))
				mountPoint := inspect.Container.Mounts[0]
				if tc.spec.Source != "" {
					tc.expected.Name = tc.spec.Source
				}
				assertContainerMountPoint(t, mountPoint, tc.expected)

				_, err = apiClient.ContainerRemove(ctx, ctr.ID, client.ContainerRemoveOptions{
					RemoveVolumes: true,
					Force:         true,
				})
				assert.NilError(t, err)

				if tc.spec.Source != "" {
					_, err = apiClient.VolumeRemove(ctx, tc.spec.Source, client.VolumeRemoveOptions{Force: true})
					assert.NilError(t, err)
				}
			})
		}
	})

	// Verify that specifying a custom driver is still taken into account.
	t.Run("custom driver", func(t *testing.T) {
		config := container.NewTestConfig(container.WithMount(mounttypes.Mount{
			Type:   mounttypes.TypeVolume,
			Target: "/foo",
			VolumeOptions: &mounttypes.VolumeOptions{
				DriverConfig: &mounttypes.Driver{
					Name: testNonExistingPlugin,
				},
			},
		}))
		_, err := apiClient.ContainerCreate(ctx, client.ContainerCreateOptions{
			Config:           config.Config,
			HostConfig:       config.HostConfig,
			NetworkingConfig: config.NetworkingConfig,
			Platform:         config.Platform,
			Name:             config.Name,
		})
		// We use [testNonExistingPlugin] for this, which produces an error
		// when used, which we use as indicator that the driver was passed
		// through. We should have a cleaner way for this, but that would
		// require a custom volume plugin to be installed.
		assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
		assert.Check(t, is.ErrorContains(err, fmt.Sprintf(`plugin %q not found`, testNonExistingPlugin)))
	})
}

// Regression test for #38995 and #43390.
func TestContainerCopyLeaksMounts(t *testing.T) {
	ctx := setupTest(t)

	bindMount := mounttypes.Mount{
		Type:   mounttypes.TypeBind,
		Source: "/var",
		Target: "/hostvar",
		BindOptions: &mounttypes.BindOptions{
			Propagation: mounttypes.PropagationRSlave,
		},
	}

	apiClient := testEnv.APIClient()
	cid := container.Run(ctx, t, apiClient, container.WithMount(bindMount), container.WithCmd("sleep", "120s"))

	getMounts := func() string {
		t.Helper()
		res, err := container.Exec(ctx, apiClient, cid, []string{"cat", "/proc/self/mountinfo"})
		assert.NilError(t, err)
		assert.Equal(t, res.ExitCode, 0)
		return res.Stdout()
	}

	mountsBefore := getMounts()

	_, err := apiClient.CopyFromContainer(ctx, cid, client.CopyFromContainerOptions{SourcePath: "/etc/passwd"})
	assert.NilError(t, err)

	mountsAfter := getMounts()

	assert.Equal(t, mountsBefore, mountsAfter)
}

func TestContainerBindMountReadOnlyDefault(t *testing.T) {
	skip.If(t, testEnv.IsRemoteDaemon)
	skip.If(t, !isRROSupported(), "requires recursive read-only mounts")

	ctx := setupTest(t)

	// The test will run a container with a simple readonly /dev bind mount (-v /dev:/dev:ro)
	// It will then check /proc/self/mountinfo for the mount type of /dev/shm (submount of /dev)
	// If /dev/shm is rw, that will mean that the read-only mounts are NOT recursive by default.
	const nonRecursive = " /dev/shm rw,"
	// If /dev/shm is ro, that will mean that the read-only mounts ARE recursive by default.
	const recursive = " /dev/shm ro,"

	for _, tc := range []struct {
		clientVersion string
		expectedOut   string
		name          string
	}{
		{clientVersion: "", expectedOut: recursive, name: "latest should be the same as 1.44"},
		{clientVersion: "1.44", expectedOut: recursive, name: "submount should be recursive by default on 1.44"},

		{clientVersion: "1.43", expectedOut: nonRecursive, name: "older than 1.44 should be non-recursive by default"},

		// TODO: Remove when DefaultMinAPIVersion >= 1.44
		{clientVersion: config.MinAPIVersion, expectedOut: nonRecursive, name: "minimum API should be non-recursive by default"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			apiClient := testEnv.APIClient()

			minDaemonVersion := tc.clientVersion
			if minDaemonVersion == "" {
				minDaemonVersion = "1.44"
			}
			skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), minDaemonVersion), "requires API v"+minDaemonVersion)

			if tc.clientVersion != "" {
				c, err := client.New(client.FromEnv, client.WithAPIVersion(tc.clientVersion))
				assert.NilError(t, err, "failed to create client with version v%s", tc.clientVersion)
				apiClient = c
			}

			for _, tc2 := range []struct {
				subname  string
				mountOpt func(*container.TestContainerConfig)
			}{
				{"mount", container.WithMount(mounttypes.Mount{
					Type:     mounttypes.TypeBind,
					Source:   "/dev",
					Target:   "/dev",
					ReadOnly: true,
				})},
				{"bind mount", container.WithBindRaw("/dev:/dev:ro")},
			} {
				t.Run(tc2.subname, func(t *testing.T) {
					cid := container.Run(ctx, t, apiClient, tc2.mountOpt,
						container.WithCmd("sh", "-c", "grep /dev/shm /proc/self/mountinfo"),
					)
					out, err := container.Output(ctx, apiClient, cid)
					assert.NilError(t, err)

					assert.Check(t, is.Equal(out.Stderr, ""))
					// Output should be either:
					// 545 526 0:160 / /dev/shm ro,nosuid,nodev,noexec,relatime shared:90 - tmpfs shm rw,size=65536k
					// or
					// 545 526 0:160 / /dev/shm rw,nosuid,nodev,noexec,relatime shared:90 - tmpfs shm rw,size=65536k
					assert.Check(t, is.Contains(out.Stdout, tc.expectedOut))
				})
			}
		})
	}
}

func TestContainerBindMountRecursivelyReadOnly(t *testing.T) {
	skip.If(t, testEnv.IsRemoteDaemon)
	skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.44"), "requires API v1.44")

	ctx := setupTest(t)

	// 0o777 for allowing rootless containers to write to this directory
	tmpDir1 := fs.NewDir(t, "tmpdir1", fs.WithMode(0o777),
		fs.WithDir("mnt", fs.WithMode(0o777)))
	defer tmpDir1.Remove()
	tmpDir1Mnt := filepath.Join(tmpDir1.Path(), "mnt")
	tmpDir2 := fs.NewDir(t, "tmpdir2", fs.WithMode(0o777),
		fs.WithFile("file", "should not be writable when recursively read only", fs.WithMode(0o666)))
	defer tmpDir2.Remove()

	if err := mount.Mount(tmpDir2.Path(), tmpDir1Mnt, "none", "bind"); err != nil {
		t.Fatal(err)
	}
	defer func() {
		if err := mount.Unmount(tmpDir1Mnt); err != nil {
			t.Fatal(err)
		}
	}()

	rroSupported := isRROSupported()

	nonRecursiveVerifier := []string{`/bin/sh`, `-xc`, `touch /foo/mnt/file; [ $? = 0 ]`}
	forceRecursiveVerifier := []string{`/bin/sh`, `-xc`, `touch /foo/mnt/file; [ $? != 0 ]`}

	// ro (recursive if kernel >= 5.12)
	ro := mounttypes.Mount{
		Type:     mounttypes.TypeBind,
		Source:   tmpDir1.Path(),
		Target:   "/foo",
		ReadOnly: true,
		BindOptions: &mounttypes.BindOptions{
			Propagation: mounttypes.PropagationRPrivate,
		},
	}
	roAsStr := ro.Source + ":" + ro.Target + ":ro,rprivate"
	roVerifier := nonRecursiveVerifier
	if rroSupported {
		roVerifier = forceRecursiveVerifier
	}

	// Non-recursive
	nonRecursive := ro
	nonRecursive.BindOptions = &mounttypes.BindOptions{
		ReadOnlyNonRecursive: true,
		Propagation:          mounttypes.PropagationRPrivate,
	}

	// Force recursive
	forceRecursive := ro
	forceRecursive.BindOptions = &mounttypes.BindOptions{
		ReadOnlyForceRecursive: true,
		Propagation:            mounttypes.PropagationRPrivate,
	}

	apiClient := testEnv.APIClient()

	containers := []string{
		container.Run(ctx, t, apiClient, container.WithMount(ro), container.WithCmd(roVerifier...), container.WithSecurityOpt("label=disable")),
		container.Run(ctx, t, apiClient, container.WithBindRaw(roAsStr), container.WithCmd(roVerifier...), container.WithSecurityOpt("label=disable")),

		container.Run(ctx, t, apiClient, container.WithMount(nonRecursive), container.WithCmd(nonRecursiveVerifier...), container.WithSecurityOpt("label=disable")),
	}

	if rroSupported {
		containers = append(containers,
			container.Run(ctx, t, apiClient, container.WithMount(forceRecursive), container.WithCmd(forceRecursiveVerifier...), container.WithSecurityOpt("label=disable")),
		)
	}

	for _, c := range containers {
		poll.WaitOn(t, container.IsSuccessful(ctx, apiClient, c))
	}
}

func isRROSupported() bool {
	return kernel.CheckKernelVersion(5, 12, 0)
}
