github.com/moby/docker@v26.1.3+incompatible/integration/daemon/daemon_test.go (about)

     1  package daemon // import "github.com/docker/docker/integration/daemon"
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"net/http/httptest"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"runtime"
    13  	"strings"
    14  	"syscall"
    15  	"testing"
    16  
    17  	containertypes "github.com/docker/docker/api/types/container"
    18  	"github.com/docker/docker/api/types/image"
    19  	"github.com/docker/docker/api/types/mount"
    20  	"github.com/docker/docker/api/types/volume"
    21  	"github.com/docker/docker/daemon/config"
    22  	"github.com/docker/docker/errdefs"
    23  	"github.com/docker/docker/integration/internal/container"
    24  	"github.com/docker/docker/integration/internal/process"
    25  	"github.com/docker/docker/pkg/stdcopy"
    26  	"github.com/docker/docker/testutil"
    27  	"github.com/docker/docker/testutil/daemon"
    28  	"gotest.tools/v3/assert"
    29  	is "gotest.tools/v3/assert/cmp"
    30  	"gotest.tools/v3/icmd"
    31  	"gotest.tools/v3/poll"
    32  	"gotest.tools/v3/skip"
    33  )
    34  
    35  func TestConfigDaemonID(t *testing.T) {
    36  	skip.If(t, runtime.GOOS == "windows")
    37  
    38  	_ = testutil.StartSpan(baseContext, t)
    39  
    40  	d := daemon.New(t)
    41  	defer d.Stop(t)
    42  
    43  	d.Start(t, "--iptables=false")
    44  	info := d.Info(t)
    45  	assert.Check(t, info.ID != "")
    46  	d.Stop(t)
    47  
    48  	// Verify that (if present) the engine-id file takes precedence
    49  	const engineID = "this-is-the-engine-id"
    50  	idFile := filepath.Join(d.RootDir(), "engine-id")
    51  	assert.Check(t, os.Remove(idFile))
    52  	// Using 0644 to allow rootless daemons to read the file (ideally
    53  	// we'd chown the file to have the remapped user as owner).
    54  	err := os.WriteFile(idFile, []byte(engineID), 0o644)
    55  	assert.NilError(t, err)
    56  
    57  	d.Start(t, "--iptables=false")
    58  	info = d.Info(t)
    59  	assert.Equal(t, info.ID, engineID)
    60  	d.Stop(t)
    61  }
    62  
    63  func TestDaemonConfigValidation(t *testing.T) {
    64  	skip.If(t, runtime.GOOS == "windows")
    65  	ctx := testutil.StartSpan(baseContext, t)
    66  
    67  	d := daemon.New(t)
    68  	dockerBinary, err := d.BinaryPath()
    69  	assert.NilError(t, err)
    70  	params := []string{"--validate", "--config-file"}
    71  
    72  	dest := os.Getenv("DOCKER_INTEGRATION_DAEMON_DEST")
    73  	if dest == "" {
    74  		dest = os.Getenv("DEST")
    75  	}
    76  	testdata := filepath.Join(dest, "..", "..", "integration", "daemon", "testdata")
    77  
    78  	const (
    79  		validOut  = "configuration OK"
    80  		failedOut = "unable to configure the Docker daemon with file"
    81  	)
    82  
    83  	tests := []struct {
    84  		name        string
    85  		args        []string
    86  		expectedOut string
    87  	}{
    88  		{
    89  			name:        "config with no content",
    90  			args:        append(params, filepath.Join(testdata, "empty-config-1.json")),
    91  			expectedOut: validOut,
    92  		},
    93  		{
    94  			name:        "config with {}",
    95  			args:        append(params, filepath.Join(testdata, "empty-config-2.json")),
    96  			expectedOut: validOut,
    97  		},
    98  		{
    99  			name:        "invalid config",
   100  			args:        append(params, filepath.Join(testdata, "invalid-config-1.json")),
   101  			expectedOut: failedOut,
   102  		},
   103  		{
   104  			name:        "malformed config",
   105  			args:        append(params, filepath.Join(testdata, "malformed-config.json")),
   106  			expectedOut: failedOut,
   107  		},
   108  		{
   109  			name:        "valid config",
   110  			args:        append(params, filepath.Join(testdata, "valid-config-1.json")),
   111  			expectedOut: validOut,
   112  		},
   113  	}
   114  	for _, tc := range tests {
   115  		tc := tc
   116  		t.Run(tc.name, func(t *testing.T) {
   117  			t.Parallel()
   118  			_ = testutil.StartSpan(ctx, t)
   119  			cmd := exec.Command(dockerBinary, tc.args...)
   120  			out, err := cmd.CombinedOutput()
   121  			assert.Check(t, is.Contains(string(out), tc.expectedOut))
   122  			if tc.expectedOut == failedOut {
   123  				assert.ErrorContains(t, err, "", "expected an error, but got none")
   124  			} else {
   125  				assert.NilError(t, err)
   126  			}
   127  		})
   128  	}
   129  }
   130  
   131  func TestConfigDaemonSeccompProfiles(t *testing.T) {
   132  	skip.If(t, runtime.GOOS == "windows")
   133  	ctx := testutil.StartSpan(baseContext, t)
   134  
   135  	d := daemon.New(t)
   136  	defer d.Stop(t)
   137  
   138  	tests := []struct {
   139  		doc             string
   140  		profile         string
   141  		expectedProfile string
   142  	}{
   143  		{
   144  			doc:             "empty profile set",
   145  			profile:         "",
   146  			expectedProfile: config.SeccompProfileDefault,
   147  		},
   148  		{
   149  			doc:             "default profile",
   150  			profile:         config.SeccompProfileDefault,
   151  			expectedProfile: config.SeccompProfileDefault,
   152  		},
   153  		{
   154  			doc:             "unconfined profile",
   155  			profile:         config.SeccompProfileUnconfined,
   156  			expectedProfile: config.SeccompProfileUnconfined,
   157  		},
   158  	}
   159  
   160  	for _, tc := range tests {
   161  		tc := tc
   162  		t.Run(tc.doc, func(t *testing.T) {
   163  			_ = testutil.StartSpan(ctx, t)
   164  
   165  			d.Start(t, "--seccomp-profile="+tc.profile)
   166  			info := d.Info(t)
   167  			assert.Assert(t, is.Contains(info.SecurityOptions, "name=seccomp,profile="+tc.expectedProfile))
   168  			d.Stop(t)
   169  
   170  			cfg := filepath.Join(d.RootDir(), "daemon.json")
   171  			err := os.WriteFile(cfg, []byte(`{"seccomp-profile": "`+tc.profile+`"}`), 0o644)
   172  			assert.NilError(t, err)
   173  
   174  			d.Start(t, "--config-file", cfg)
   175  			info = d.Info(t)
   176  			assert.Assert(t, is.Contains(info.SecurityOptions, "name=seccomp,profile="+tc.expectedProfile))
   177  			d.Stop(t)
   178  		})
   179  	}
   180  }
   181  
   182  func TestDaemonProxy(t *testing.T) {
   183  	skip.If(t, runtime.GOOS == "windows", "cannot start multiple daemons on windows")
   184  	skip.If(t, os.Getenv("DOCKER_ROOTLESS") != "", "cannot connect to localhost proxy in rootless environment")
   185  	ctx := testutil.StartSpan(baseContext, t)
   186  
   187  	newProxy := func(rcvd *string, t *testing.T) *httptest.Server {
   188  		s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   189  			*rcvd = r.Host
   190  			w.Header().Set("Content-Type", "application/json")
   191  			_, _ = w.Write([]byte("OK"))
   192  		}))
   193  		t.Cleanup(s.Close)
   194  		return s
   195  	}
   196  
   197  	const userPass = "myuser:mypassword@"
   198  
   199  	// Configure proxy through env-vars
   200  	t.Run("environment variables", func(t *testing.T) {
   201  		t.Parallel()
   202  
   203  		ctx := testutil.StartSpan(ctx, t)
   204  		var received string
   205  		proxyServer := newProxy(&received, t)
   206  
   207  		d := daemon.New(t, daemon.WithEnvVars(
   208  			"HTTP_PROXY="+proxyServer.URL,
   209  			"HTTPS_PROXY="+proxyServer.URL,
   210  			"NO_PROXY=example.com",
   211  			"OTEL_EXPORTER_OTLP_ENDPOINT=", // To avoid OTEL hitting the proxy.
   212  		))
   213  		c := d.NewClientT(t)
   214  
   215  		d.Start(t, "--iptables=false")
   216  		defer d.Stop(t)
   217  
   218  		info := d.Info(t)
   219  		assert.Check(t, is.Equal(info.HTTPProxy, proxyServer.URL))
   220  		assert.Check(t, is.Equal(info.HTTPSProxy, proxyServer.URL))
   221  		assert.Check(t, is.Equal(info.NoProxy, "example.com"))
   222  
   223  		_, err := c.ImagePull(ctx, "example.org:5000/some/image:latest", image.PullOptions{})
   224  		assert.ErrorContains(t, err, "", "pulling should have failed")
   225  		assert.Equal(t, received, "example.org:5000")
   226  
   227  		// Test NoProxy: example.com should not hit the proxy, and "received" variable should not be changed.
   228  		_, err = c.ImagePull(ctx, "example.com/some/image:latest", image.PullOptions{})
   229  		assert.ErrorContains(t, err, "", "pulling should have failed")
   230  		assert.Equal(t, received, "example.org:5000", "should not have used proxy")
   231  	})
   232  
   233  	// Configure proxy through command-line flags
   234  	t.Run("command-line options", func(t *testing.T) {
   235  		t.Parallel()
   236  
   237  		ctx := testutil.StartSpan(ctx, t)
   238  
   239  		var received string
   240  		proxyServer := newProxy(&received, t)
   241  
   242  		d := daemon.New(t, daemon.WithEnvVars(
   243  			"HTTP_PROXY="+"http://"+userPass+"from-env-http.invalid",
   244  			"http_proxy="+"http://"+userPass+"from-env-http.invalid",
   245  			"HTTPS_PROXY="+"https://"+userPass+"myuser:mypassword@from-env-https-invalid",
   246  			"https_proxy="+"https://"+userPass+"myuser:mypassword@from-env-https-invalid",
   247  			"NO_PROXY=ignore.invalid",
   248  			"no_proxy=ignore.invalid",
   249  			"OTEL_EXPORTER_OTLP_ENDPOINT=", // To avoid OTEL hitting the proxy.
   250  		))
   251  		d.Start(t, "--iptables=false", "--http-proxy", proxyServer.URL, "--https-proxy", proxyServer.URL, "--no-proxy", "example.com")
   252  		defer d.Stop(t)
   253  
   254  		c := d.NewClientT(t)
   255  
   256  		info := d.Info(t)
   257  		assert.Check(t, is.Equal(info.HTTPProxy, proxyServer.URL))
   258  		assert.Check(t, is.Equal(info.HTTPSProxy, proxyServer.URL))
   259  		assert.Check(t, is.Equal(info.NoProxy, "example.com"))
   260  
   261  		ok, _ := d.ScanLogsT(ctx, t, daemon.ScanLogsMatchAll(
   262  			"overriding existing proxy variable with value from configuration",
   263  			"http_proxy",
   264  			"HTTP_PROXY",
   265  			"https_proxy",
   266  			"HTTPS_PROXY",
   267  			"no_proxy",
   268  			"NO_PROXY",
   269  		))
   270  		assert.Assert(t, ok)
   271  
   272  		ok, logs := d.ScanLogsT(ctx, t, daemon.ScanLogsMatchString(userPass))
   273  		assert.Assert(t, !ok, "logs should not contain the non-sanitized proxy URL: %s", logs)
   274  
   275  		_, err := c.ImagePull(ctx, "example.org:5001/some/image:latest", image.PullOptions{})
   276  		assert.ErrorContains(t, err, "", "pulling should have failed")
   277  		assert.Equal(t, received, "example.org:5001")
   278  
   279  		// Test NoProxy: example.com should not hit the proxy, and "received" variable should not be changed.
   280  		_, err = c.ImagePull(ctx, "example.com/some/image:latest", image.PullOptions{})
   281  		assert.ErrorContains(t, err, "", "pulling should have failed")
   282  		assert.Equal(t, received, "example.org:5001", "should not have used proxy")
   283  	})
   284  
   285  	// Configure proxy through configuration file
   286  	t.Run("configuration file", func(t *testing.T) {
   287  		t.Parallel()
   288  		ctx := testutil.StartSpan(ctx, t)
   289  
   290  		var received string
   291  		proxyServer := newProxy(&received, t)
   292  
   293  		d := daemon.New(t, daemon.WithEnvVars(
   294  			"HTTP_PROXY="+"http://"+userPass+"from-env-http.invalid",
   295  			"http_proxy="+"http://"+userPass+"from-env-http.invalid",
   296  			"HTTPS_PROXY="+"https://"+userPass+"myuser:mypassword@from-env-https-invalid",
   297  			"https_proxy="+"https://"+userPass+"myuser:mypassword@from-env-https-invalid",
   298  			"NO_PROXY=ignore.invalid",
   299  			"no_proxy=ignore.invalid",
   300  			"OTEL_EXPORTER_OTLP_ENDPOINT=", // To avoid OTEL hitting the proxy.
   301  		))
   302  		c := d.NewClientT(t)
   303  
   304  		configFile := filepath.Join(d.RootDir(), "daemon.json")
   305  		configJSON := fmt.Sprintf(`{"proxies":{"http-proxy":%[1]q, "https-proxy": %[1]q, "no-proxy": "example.com"}}`, proxyServer.URL)
   306  		assert.NilError(t, os.WriteFile(configFile, []byte(configJSON), 0o644))
   307  
   308  		d.Start(t, "--iptables=false", "--config-file", configFile)
   309  		defer d.Stop(t)
   310  
   311  		info := d.Info(t)
   312  		assert.Check(t, is.Equal(info.HTTPProxy, proxyServer.URL))
   313  		assert.Check(t, is.Equal(info.HTTPSProxy, proxyServer.URL))
   314  		assert.Check(t, is.Equal(info.NoProxy, "example.com"))
   315  
   316  		d.ScanLogsT(ctx, t, daemon.ScanLogsMatchAll(
   317  			"overriding existing proxy variable with value from configuration",
   318  			"http_proxy",
   319  			"HTTP_PROXY",
   320  			"https_proxy",
   321  			"HTTPS_PROXY",
   322  			"no_proxy",
   323  			"NO_PROXY",
   324  		))
   325  
   326  		_, err := c.ImagePull(ctx, "example.org:5002/some/image:latest", image.PullOptions{})
   327  		assert.ErrorContains(t, err, "", "pulling should have failed")
   328  		assert.Equal(t, received, "example.org:5002")
   329  
   330  		// Test NoProxy: example.com should not hit the proxy, and "received" variable should not be changed.
   331  		_, err = c.ImagePull(ctx, "example.com/some/image:latest", image.PullOptions{})
   332  		assert.ErrorContains(t, err, "", "pulling should have failed")
   333  		assert.Equal(t, received, "example.org:5002", "should not have used proxy")
   334  	})
   335  
   336  	// Conflicting options (passed both through command-line options and config file)
   337  	t.Run("conflicting options", func(t *testing.T) {
   338  		ctx := testutil.StartSpan(ctx, t)
   339  		const (
   340  			proxyRawURL = "https://" + userPass + "example.org"
   341  			proxyURL    = "https://xxxxx:xxxxx@example.org"
   342  		)
   343  
   344  		d := daemon.New(t)
   345  
   346  		configFile := filepath.Join(d.RootDir(), "daemon.json")
   347  		configJSON := fmt.Sprintf(`{"proxies":{"http-proxy":%[1]q, "https-proxy": %[1]q, "no-proxy": "example.com"}}`, proxyRawURL)
   348  		assert.NilError(t, os.WriteFile(configFile, []byte(configJSON), 0o644))
   349  
   350  		err := d.StartWithError("--http-proxy", proxyRawURL, "--https-proxy", proxyRawURL, "--no-proxy", "example.com", "--config-file", configFile, "--validate")
   351  		assert.ErrorContains(t, err, "daemon exited during startup")
   352  
   353  		expected := fmt.Sprintf(
   354  			`the following directives are specified both as a flag and in the configuration file: http-proxy: (from flag: %[1]s, from file: %[1]s), https-proxy: (from flag: %[1]s, from file: %[1]s), no-proxy: (from flag: example.com, from file: example.com)`,
   355  			proxyURL,
   356  		)
   357  		poll.WaitOn(t, d.PollCheckLogs(ctx, daemon.ScanLogsMatchString(expected)))
   358  	})
   359  
   360  	// Make sure values are sanitized when reloading the daemon-config
   361  	t.Run("reload sanitized", func(t *testing.T) {
   362  		t.Parallel()
   363  		ctx := testutil.StartSpan(ctx, t)
   364  
   365  		const (
   366  			proxyRawURL = "https://" + userPass + "example.org"
   367  			proxyURL    = "https://xxxxx:xxxxx@example.org"
   368  		)
   369  
   370  		d := daemon.New(t, daemon.WithEnvVars(
   371  			"OTEL_EXPORTER_OTLP_ENDPOINT=", // To avoid OTEL hitting the proxy.
   372  		))
   373  		d.Start(t, "--iptables=false", "--http-proxy", proxyRawURL, "--https-proxy", proxyRawURL, "--no-proxy", "example.com")
   374  		defer d.Stop(t)
   375  		err := d.Signal(syscall.SIGHUP)
   376  		assert.NilError(t, err)
   377  
   378  		poll.WaitOn(t, d.PollCheckLogs(ctx, daemon.ScanLogsMatchAll("Reloaded configuration:", proxyURL)))
   379  
   380  		ok, logs := d.ScanLogsT(ctx, t, daemon.ScanLogsMatchString(userPass))
   381  		assert.Assert(t, !ok, "logs should not contain the non-sanitized proxy URL: %s", logs)
   382  	})
   383  }
   384  
   385  func TestLiveRestore(t *testing.T) {
   386  	skip.If(t, runtime.GOOS == "windows", "cannot start multiple daemons on windows")
   387  	_ = testutil.StartSpan(baseContext, t)
   388  
   389  	t.Run("volume references", testLiveRestoreVolumeReferences)
   390  	t.Run("autoremove", testLiveRestoreAutoRemove)
   391  }
   392  
   393  func testLiveRestoreAutoRemove(t *testing.T) {
   394  	skip.If(t, testEnv.IsRootless(), "restarted rootless daemon will have a new process namespace")
   395  
   396  	t.Parallel()
   397  	ctx := testutil.StartSpan(baseContext, t)
   398  
   399  	run := func(t *testing.T) (*daemon.Daemon, func(), string) {
   400  		d := daemon.New(t)
   401  		d.StartWithBusybox(ctx, t, "--live-restore", "--iptables=false")
   402  		t.Cleanup(func() {
   403  			d.Stop(t)
   404  			d.Cleanup(t)
   405  		})
   406  
   407  		tmpDir := t.TempDir()
   408  
   409  		apiClient := d.NewClientT(t)
   410  
   411  		cID := container.Run(ctx, t, apiClient,
   412  			container.WithBind(tmpDir, "/v"),
   413  			// Run until a 'stop' file is created.
   414  			container.WithCmd("sh", "-c", "while [ ! -f /v/stop ]; do sleep 0.1; done"),
   415  			container.WithAutoRemove)
   416  		t.Cleanup(func() { apiClient.ContainerRemove(ctx, cID, containertypes.RemoveOptions{Force: true}) })
   417  		finishContainer := func() {
   418  			file, err := os.Create(filepath.Join(tmpDir, "stop"))
   419  			assert.NilError(t, err, "Failed to create 'stop' file")
   420  			file.Close()
   421  		}
   422  		return d, finishContainer, cID
   423  	}
   424  
   425  	t.Run("engine restart shouldnt kill alive containers", func(t *testing.T) {
   426  		d, finishContainer, cID := run(t)
   427  
   428  		d.Restart(t, "--live-restore", "--iptables=false")
   429  
   430  		apiClient := d.NewClientT(t)
   431  		_, err := apiClient.ContainerInspect(ctx, cID)
   432  		assert.NilError(t, err, "Container shouldn't be removed after engine restart")
   433  
   434  		finishContainer()
   435  
   436  		poll.WaitOn(t, container.IsRemoved(ctx, apiClient, cID))
   437  	})
   438  	t.Run("engine restart should remove containers that exited", func(t *testing.T) {
   439  		d, finishContainer, cID := run(t)
   440  
   441  		apiClient := d.NewClientT(t)
   442  
   443  		// Get PID of the container process.
   444  		inspect, err := apiClient.ContainerInspect(ctx, cID)
   445  		assert.NilError(t, err)
   446  		pid := inspect.State.Pid
   447  
   448  		d.Stop(t)
   449  
   450  		finishContainer()
   451  		poll.WaitOn(t, process.NotAlive(pid))
   452  
   453  		d.Start(t, "--live-restore", "--iptables=false")
   454  
   455  		poll.WaitOn(t, container.IsRemoved(ctx, apiClient, cID))
   456  	})
   457  }
   458  
   459  func testLiveRestoreVolumeReferences(t *testing.T) {
   460  	t.Parallel()
   461  	ctx := testutil.StartSpan(baseContext, t)
   462  
   463  	d := daemon.New(t)
   464  	d.StartWithBusybox(ctx, t, "--live-restore", "--iptables=false")
   465  	defer func() {
   466  		d.Stop(t)
   467  		d.Cleanup(t)
   468  	}()
   469  
   470  	c := d.NewClientT(t)
   471  
   472  	runTest := func(t *testing.T, policy containertypes.RestartPolicyMode) {
   473  		t.Run(string(policy), func(t *testing.T) {
   474  			ctx := testutil.StartSpan(ctx, t)
   475  			volName := "test-live-restore-volume-references-" + string(policy)
   476  			_, err := c.VolumeCreate(ctx, volume.CreateOptions{Name: volName})
   477  			assert.NilError(t, err)
   478  
   479  			// Create a container that uses the volume
   480  			m := mount.Mount{
   481  				Type:   mount.TypeVolume,
   482  				Source: volName,
   483  				Target: "/foo",
   484  			}
   485  			cID := container.Run(ctx, t, c, container.WithMount(m), container.WithCmd("top"), container.WithRestartPolicy(policy))
   486  			defer c.ContainerRemove(ctx, cID, containertypes.RemoveOptions{Force: true})
   487  
   488  			// Stop the daemon
   489  			d.Restart(t, "--live-restore", "--iptables=false")
   490  
   491  			// Try to remove the volume
   492  			err = c.VolumeRemove(ctx, volName, false)
   493  			assert.ErrorContains(t, err, "volume is in use")
   494  
   495  			_, err = c.VolumeInspect(ctx, volName)
   496  			assert.NilError(t, err)
   497  		})
   498  	}
   499  
   500  	t.Run("restartPolicy", func(t *testing.T) {
   501  		runTest(t, containertypes.RestartPolicyAlways)
   502  		runTest(t, containertypes.RestartPolicyUnlessStopped)
   503  		runTest(t, containertypes.RestartPolicyOnFailure)
   504  		runTest(t, containertypes.RestartPolicyDisabled)
   505  	})
   506  
   507  	// Make sure that the local volume driver's mount ref count is restored
   508  	// Addresses https://github.com/moby/moby/issues/44422
   509  	t.Run("local volume with mount options", func(t *testing.T) {
   510  		ctx := testutil.StartSpan(ctx, t)
   511  		v, err := c.VolumeCreate(ctx, volume.CreateOptions{
   512  			Driver: "local",
   513  			Name:   "test-live-restore-volume-references-local",
   514  			DriverOpts: map[string]string{
   515  				"type":   "tmpfs",
   516  				"device": "tmpfs",
   517  			},
   518  		})
   519  		assert.NilError(t, err)
   520  		m := mount.Mount{
   521  			Type:   mount.TypeVolume,
   522  			Source: v.Name,
   523  			Target: "/foo",
   524  		}
   525  
   526  		const testContent = "hello"
   527  		cID := container.Run(ctx, t, c, container.WithMount(m), container.WithCmd("sh", "-c", "echo "+testContent+">>/foo/test.txt; sleep infinity"))
   528  		defer c.ContainerRemove(ctx, cID, containertypes.RemoveOptions{Force: true})
   529  
   530  		// Wait until container creates a file in the volume.
   531  		poll.WaitOn(t, func(t poll.LogT) poll.Result {
   532  			stat, err := c.ContainerStatPath(ctx, cID, "/foo/test.txt")
   533  			if err != nil {
   534  				if errdefs.IsNotFound(err) {
   535  					return poll.Continue("file doesn't yet exist")
   536  				}
   537  				return poll.Error(err)
   538  			}
   539  
   540  			if int(stat.Size) != len(testContent)+1 {
   541  				return poll.Error(fmt.Errorf("unexpected test file size: %d", stat.Size))
   542  			}
   543  
   544  			return poll.Success()
   545  		})
   546  
   547  		d.Restart(t, "--live-restore", "--iptables=false")
   548  
   549  		// Try to remove the volume
   550  		// This should fail since its used by a container
   551  		err = c.VolumeRemove(ctx, v.Name, false)
   552  		assert.ErrorContains(t, err, "volume is in use")
   553  
   554  		t.Run("volume still mounted", func(t *testing.T) {
   555  			skip.If(t, testEnv.IsRootless(), "restarted rootless daemon has a new mount namespace and it won't have the previous mounts")
   556  
   557  			// Check if a new container with the same volume has access to the previous content.
   558  			// This fails if the volume gets unmounted at startup.
   559  			cID2 := container.Run(ctx, t, c, container.WithMount(m), container.WithCmd("cat", "/foo/test.txt"))
   560  			defer c.ContainerRemove(ctx, cID2, containertypes.RemoveOptions{Force: true})
   561  
   562  			poll.WaitOn(t, container.IsStopped(ctx, c, cID2))
   563  
   564  			inspect, err := c.ContainerInspect(ctx, cID2)
   565  			if assert.Check(t, err) {
   566  				assert.Check(t, is.Equal(inspect.State.ExitCode, 0), "volume doesn't have the same file")
   567  			}
   568  
   569  			logs, err := c.ContainerLogs(ctx, cID2, containertypes.LogsOptions{ShowStdout: true})
   570  			assert.NilError(t, err)
   571  			defer logs.Close()
   572  
   573  			var stdoutBuf bytes.Buffer
   574  			_, err = stdcopy.StdCopy(&stdoutBuf, io.Discard, logs)
   575  			assert.NilError(t, err)
   576  
   577  			assert.Check(t, is.Equal(strings.TrimSpace(stdoutBuf.String()), testContent))
   578  		})
   579  
   580  		// Remove that container which should free the references in the volume
   581  		err = c.ContainerRemove(ctx, cID, containertypes.RemoveOptions{Force: true})
   582  		assert.NilError(t, err)
   583  
   584  		// Now we should be able to remove the volume
   585  		err = c.VolumeRemove(ctx, v.Name, false)
   586  		assert.NilError(t, err)
   587  	})
   588  
   589  	// Make sure that we don't panic if the container has bind-mounts
   590  	// (which should not be "restored")
   591  	// Regression test for https://github.com/moby/moby/issues/45898
   592  	t.Run("container with bind-mounts", func(t *testing.T) {
   593  		ctx := testutil.StartSpan(ctx, t)
   594  		m := mount.Mount{
   595  			Type:   mount.TypeBind,
   596  			Source: os.TempDir(),
   597  			Target: "/foo",
   598  		}
   599  		cID := container.Run(ctx, t, c, container.WithMount(m), container.WithCmd("top"))
   600  		defer c.ContainerRemove(ctx, cID, containertypes.RemoveOptions{Force: true})
   601  
   602  		d.Restart(t, "--live-restore", "--iptables=false")
   603  
   604  		err := c.ContainerRemove(ctx, cID, containertypes.RemoveOptions{Force: true})
   605  		assert.NilError(t, err)
   606  	})
   607  }
   608  
   609  func TestDaemonDefaultBridgeWithFixedCidrButNoBip(t *testing.T) {
   610  	skip.If(t, runtime.GOOS == "windows")
   611  
   612  	ctx := testutil.StartSpan(baseContext, t)
   613  
   614  	bridgeName := "ext-bridge1"
   615  	d := daemon.New(t, daemon.WithEnvVars("DOCKER_TEST_CREATE_DEFAULT_BRIDGE="+bridgeName))
   616  	defer func() {
   617  		d.Stop(t)
   618  		d.Cleanup(t)
   619  	}()
   620  
   621  	defer func() {
   622  		// No need to clean up when running this test in rootless mode, as the
   623  		// interface is deleted when the daemon is stopped and the netns
   624  		// reclaimed by the kernel.
   625  		if !testEnv.IsRootless() {
   626  			deleteInterface(t, bridgeName)
   627  		}
   628  	}()
   629  	d.StartWithBusybox(ctx, t, "--bridge", bridgeName, "--fixed-cidr", "192.168.130.0/24")
   630  }
   631  
   632  func deleteInterface(t *testing.T, ifName string) {
   633  	icmd.RunCommand("ip", "link", "delete", ifName).Assert(t, icmd.Success)
   634  	icmd.RunCommand("iptables", "-t", "nat", "--flush").Assert(t, icmd.Success)
   635  	icmd.RunCommand("iptables", "--flush").Assert(t, icmd.Success)
   636  }