package daemon // import "github.com/docker/docker/integration/daemon" import ( "bytes" "context" "fmt" "io" "net/http" "net/http/httptest" "os" "os/exec" "path/filepath" "runtime" "strings" "syscall" "testing" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/volume" "github.com/docker/docker/daemon/config" "github.com/docker/docker/errdefs" "github.com/docker/docker/integration/internal/container" "github.com/docker/docker/integration/internal/process" "github.com/docker/docker/pkg/stdcopy" "github.com/docker/docker/testutil/daemon" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/icmd" "gotest.tools/v3/poll" "gotest.tools/v3/skip" ) func TestConfigDaemonID(t *testing.T) { skip.If(t, runtime.GOOS == "windows") d := daemon.New(t) defer d.Stop(t) d.Start(t, "--iptables=false") info := d.Info(t) assert.Check(t, info.ID != "") d.Stop(t) // Verify that (if present) the engine-id file takes precedence const engineID = "this-is-the-engine-id" idFile := filepath.Join(d.RootDir(), "engine-id") assert.Check(t, os.Remove(idFile)) // Using 0644 to allow rootless daemons to read the file (ideally // we'd chown the file to have the remapped user as owner). err := os.WriteFile(idFile, []byte(engineID), 0o644) assert.NilError(t, err) d.Start(t, "--iptables=false") info = d.Info(t) assert.Equal(t, info.ID, engineID) d.Stop(t) } func TestDaemonConfigValidation(t *testing.T) { skip.If(t, runtime.GOOS == "windows") d := daemon.New(t) dockerBinary, err := d.BinaryPath() assert.NilError(t, err) params := []string{"--validate", "--config-file"} dest := os.Getenv("DOCKER_INTEGRATION_DAEMON_DEST") if dest == "" { dest = os.Getenv("DEST") } testdata := filepath.Join(dest, "..", "..", "integration", "daemon", "testdata") const ( validOut = "configuration OK" failedOut = "unable to configure the Docker daemon with file" ) tests := []struct { name string args []string expectedOut string }{ { name: "config with no content", args: append(params, filepath.Join(testdata, "empty-config-1.json")), expectedOut: validOut, }, { name: "config with {}", args: append(params, filepath.Join(testdata, "empty-config-2.json")), expectedOut: validOut, }, { name: "invalid config", args: append(params, filepath.Join(testdata, "invalid-config-1.json")), expectedOut: failedOut, }, { name: "malformed config", args: append(params, filepath.Join(testdata, "malformed-config.json")), expectedOut: failedOut, }, { name: "valid config", args: append(params, filepath.Join(testdata, "valid-config-1.json")), expectedOut: validOut, }, } for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() cmd := exec.Command(dockerBinary, tc.args...) out, err := cmd.CombinedOutput() assert.Check(t, is.Contains(string(out), tc.expectedOut)) if tc.expectedOut == failedOut { assert.ErrorContains(t, err, "", "expected an error, but got none") } else { assert.NilError(t, err) } }) } } func TestConfigDaemonSeccompProfiles(t *testing.T) { skip.If(t, runtime.GOOS == "windows") d := daemon.New(t) defer d.Stop(t) tests := []struct { doc string profile string expectedProfile string }{ { doc: "empty profile set", profile: "", expectedProfile: config.SeccompProfileDefault, }, { doc: "default profile", profile: config.SeccompProfileDefault, expectedProfile: config.SeccompProfileDefault, }, { doc: "unconfined profile", profile: config.SeccompProfileUnconfined, expectedProfile: config.SeccompProfileUnconfined, }, } for _, tc := range tests { tc := tc t.Run(tc.doc, func(t *testing.T) { d.Start(t, "--seccomp-profile="+tc.profile) info := d.Info(t) assert.Assert(t, is.Contains(info.SecurityOptions, "name=seccomp,profile="+tc.expectedProfile)) d.Stop(t) cfg := filepath.Join(d.RootDir(), "daemon.json") err := os.WriteFile(cfg, []byte(`{"seccomp-profile": "`+tc.profile+`"}`), 0644) assert.NilError(t, err) d.Start(t, "--config-file", cfg) info = d.Info(t) assert.Assert(t, is.Contains(info.SecurityOptions, "name=seccomp,profile="+tc.expectedProfile)) d.Stop(t) }) } } func TestDaemonProxy(t *testing.T) { skip.If(t, runtime.GOOS == "windows", "cannot start multiple daemons on windows") skip.If(t, os.Getenv("DOCKER_ROOTLESS") != "", "cannot connect to localhost proxy in rootless environment") newProxy := func(rcvd *string, t *testing.T) *httptest.Server { s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { *rcvd = r.Host w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte("OK")) })) t.Cleanup(s.Close) return s } const userPass = "myuser:mypassword@" // Configure proxy through env-vars t.Run("environment variables", func(t *testing.T) { t.Parallel() ctx := context.Background() var received string proxyServer := newProxy(&received, t) d := daemon.New(t, daemon.WithEnvVars( "HTTP_PROXY="+proxyServer.URL, "HTTPS_PROXY="+proxyServer.URL, "NO_PROXY=example.com", )) c := d.NewClientT(t) d.Start(t, "--iptables=false") defer d.Stop(t) info := d.Info(t) assert.Check(t, is.Equal(info.HTTPProxy, proxyServer.URL)) assert.Check(t, is.Equal(info.HTTPSProxy, proxyServer.URL)) assert.Check(t, is.Equal(info.NoProxy, "example.com")) _, err := c.ImagePull(ctx, "example.org:5000/some/image:latest", types.ImagePullOptions{}) assert.ErrorContains(t, err, "", "pulling should have failed") assert.Equal(t, received, "example.org:5000") // Test NoProxy: example.com should not hit the proxy, and "received" variable should not be changed. _, err = c.ImagePull(ctx, "example.com/some/image:latest", types.ImagePullOptions{}) assert.ErrorContains(t, err, "", "pulling should have failed") assert.Equal(t, received, "example.org:5000", "should not have used proxy") }) // Configure proxy through command-line flags t.Run("command-line options", func(t *testing.T) { t.Parallel() ctx := context.Background() var received string proxyServer := newProxy(&received, t) d := daemon.New(t, daemon.WithEnvVars( "HTTP_PROXY="+"http://"+userPass+"from-env-http.invalid", "http_proxy="+"http://"+userPass+"from-env-http.invalid", "HTTPS_PROXY="+"https://"+userPass+"myuser:mypassword@from-env-https-invalid", "https_proxy="+"https://"+userPass+"myuser:mypassword@from-env-https-invalid", "NO_PROXY=ignore.invalid", "no_proxy=ignore.invalid", )) d.Start(t, "--iptables=false", "--http-proxy", proxyServer.URL, "--https-proxy", proxyServer.URL, "--no-proxy", "example.com") defer d.Stop(t) c := d.NewClientT(t) info := d.Info(t) assert.Check(t, is.Equal(info.HTTPProxy, proxyServer.URL)) assert.Check(t, is.Equal(info.HTTPSProxy, proxyServer.URL)) assert.Check(t, is.Equal(info.NoProxy, "example.com")) ok, _ := d.ScanLogsT(ctx, t, daemon.ScanLogsMatchAll( "overriding existing proxy variable with value from configuration", "http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY", "no_proxy", "NO_PROXY", )) assert.Assert(t, ok) ok, logs := d.ScanLogsT(ctx, t, daemon.ScanLogsMatchString(userPass)) assert.Assert(t, !ok, "logs should not contain the non-sanitized proxy URL: %s", logs) _, err := c.ImagePull(ctx, "example.org:5001/some/image:latest", types.ImagePullOptions{}) assert.ErrorContains(t, err, "", "pulling should have failed") assert.Equal(t, received, "example.org:5001") // Test NoProxy: example.com should not hit the proxy, and "received" variable should not be changed. _, err = c.ImagePull(ctx, "example.com/some/image:latest", types.ImagePullOptions{}) assert.ErrorContains(t, err, "", "pulling should have failed") assert.Equal(t, received, "example.org:5001", "should not have used proxy") }) // Configure proxy through configuration file t.Run("configuration file", func(t *testing.T) { t.Parallel() ctx := context.Background() var received string proxyServer := newProxy(&received, t) d := daemon.New(t, daemon.WithEnvVars( "HTTP_PROXY="+"http://"+userPass+"from-env-http.invalid", "http_proxy="+"http://"+userPass+"from-env-http.invalid", "HTTPS_PROXY="+"https://"+userPass+"myuser:mypassword@from-env-https-invalid", "https_proxy="+"https://"+userPass+"myuser:mypassword@from-env-https-invalid", "NO_PROXY=ignore.invalid", "no_proxy=ignore.invalid", )) c := d.NewClientT(t) configFile := filepath.Join(d.RootDir(), "daemon.json") configJSON := fmt.Sprintf(`{"proxies":{"http-proxy":%[1]q, "https-proxy": %[1]q, "no-proxy": "example.com"}}`, proxyServer.URL) assert.NilError(t, os.WriteFile(configFile, []byte(configJSON), 0644)) d.Start(t, "--iptables=false", "--config-file", configFile) defer d.Stop(t) info := d.Info(t) assert.Check(t, is.Equal(info.HTTPProxy, proxyServer.URL)) assert.Check(t, is.Equal(info.HTTPSProxy, proxyServer.URL)) assert.Check(t, is.Equal(info.NoProxy, "example.com")) d.ScanLogsT(ctx, t, daemon.ScanLogsMatchAll( "overriding existing proxy variable with value from configuration", "http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY", "no_proxy", "NO_PROXY", )) _, err := c.ImagePull(ctx, "example.org:5002/some/image:latest", types.ImagePullOptions{}) assert.ErrorContains(t, err, "", "pulling should have failed") assert.Equal(t, received, "example.org:5002") // Test NoProxy: example.com should not hit the proxy, and "received" variable should not be changed. _, err = c.ImagePull(ctx, "example.com/some/image:latest", types.ImagePullOptions{}) assert.ErrorContains(t, err, "", "pulling should have failed") assert.Equal(t, received, "example.org:5002", "should not have used proxy") }) // Conflicting options (passed both through command-line options and config file) t.Run("conflicting options", func(t *testing.T) { ctx := context.Background() const ( proxyRawURL = "https://" + userPass + "example.org" proxyURL = "https://xxxxx:xxxxx@example.org" ) d := daemon.New(t) configFile := filepath.Join(d.RootDir(), "daemon.json") configJSON := fmt.Sprintf(`{"proxies":{"http-proxy":%[1]q, "https-proxy": %[1]q, "no-proxy": "example.com"}}`, proxyRawURL) assert.NilError(t, os.WriteFile(configFile, []byte(configJSON), 0644)) err := d.StartWithError("--http-proxy", proxyRawURL, "--https-proxy", proxyRawURL, "--no-proxy", "example.com", "--config-file", configFile, "--validate") assert.ErrorContains(t, err, "daemon exited during startup") expected := fmt.Sprintf( `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)`, proxyURL, ) poll.WaitOn(t, d.PollCheckLogs(ctx, daemon.ScanLogsMatchString(expected))) }) // Make sure values are sanitized when reloading the daemon-config t.Run("reload sanitized", func(t *testing.T) { t.Parallel() ctx := context.Background() const ( proxyRawURL = "https://" + userPass + "example.org" proxyURL = "https://xxxxx:xxxxx@example.org" ) d := daemon.New(t) d.Start(t, "--iptables=false", "--http-proxy", proxyRawURL, "--https-proxy", proxyRawURL, "--no-proxy", "example.com") defer d.Stop(t) err := d.Signal(syscall.SIGHUP) assert.NilError(t, err) poll.WaitOn(t, d.PollCheckLogs(ctx, daemon.ScanLogsMatchAll("Reloaded configuration:", proxyURL))) ok, logs := d.ScanLogsT(ctx, t, daemon.ScanLogsMatchString(userPass)) assert.Assert(t, !ok, "logs should not contain the non-sanitized proxy URL: %s", logs) }) } func TestLiveRestore(t *testing.T) { skip.If(t, runtime.GOOS == "windows", "cannot start multiple daemons on windows") t.Run("volume references", testLiveRestoreVolumeReferences) t.Run("autoremove", testLiveRestoreAutoRemove) } func testLiveRestoreAutoRemove(t *testing.T) { skip.If(t, testEnv.IsRootless(), "restarted rootless daemon will have a new process namespace") t.Parallel() ctx := context.Background() run := func(t *testing.T) (*daemon.Daemon, func(), string) { d := daemon.New(t) d.StartWithBusybox(t, "--live-restore", "--iptables=false") t.Cleanup(func() { d.Stop(t) d.Cleanup(t) }) tmpDir := t.TempDir() apiClient := d.NewClientT(t) cID := container.Run(ctx, t, apiClient, container.WithBind(tmpDir, "/v"), // Run until a 'stop' file is created. container.WithCmd("sh", "-c", "while [ ! -f /v/stop ]; do sleep 0.1; done"), container.WithAutoRemove) t.Cleanup(func() { apiClient.ContainerRemove(ctx, cID, types.ContainerRemoveOptions{Force: true}) }) finishContainer := func() { file, err := os.Create(filepath.Join(tmpDir, "stop")) assert.NilError(t, err, "Failed to create 'stop' file") file.Close() } return d, finishContainer, cID } t.Run("engine restart shouldnt kill alive containers", func(t *testing.T) { d, finishContainer, cID := run(t) d.Restart(t, "--live-restore", "--iptables=false") apiClient := d.NewClientT(t) _, err := apiClient.ContainerInspect(ctx, cID) assert.NilError(t, err, "Container shouldn't be removed after engine restart") finishContainer() poll.WaitOn(t, container.IsRemoved(ctx, apiClient, cID)) }) t.Run("engine restart should remove containers that exited", func(t *testing.T) { d, finishContainer, cID := run(t) apiClient := d.NewClientT(t) // Get PID of the container process. inspect, err := apiClient.ContainerInspect(ctx, cID) assert.NilError(t, err) pid := inspect.State.Pid d.Stop(t) finishContainer() poll.WaitOn(t, process.NotAlive(pid)) d.Start(t, "--live-restore", "--iptables=false") poll.WaitOn(t, container.IsRemoved(ctx, apiClient, cID)) }) } func testLiveRestoreVolumeReferences(t *testing.T) { t.Parallel() d := daemon.New(t) d.StartWithBusybox(t, "--live-restore", "--iptables=false") defer func() { d.Stop(t) d.Cleanup(t) }() c := d.NewClientT(t) ctx := context.Background() runTest := func(t *testing.T, policy string) { t.Run(policy, func(t *testing.T) { volName := "test-live-restore-volume-references-" + policy _, err := c.VolumeCreate(ctx, volume.CreateOptions{Name: volName}) assert.NilError(t, err) // Create a container that uses the volume m := mount.Mount{ Type: mount.TypeVolume, Source: volName, Target: "/foo", } cID := container.Run(ctx, t, c, container.WithMount(m), container.WithCmd("top"), container.WithRestartPolicy(policy)) defer c.ContainerRemove(ctx, cID, types.ContainerRemoveOptions{Force: true}) // Stop the daemon d.Restart(t, "--live-restore", "--iptables=false") // Try to remove the volume err = c.VolumeRemove(ctx, volName, false) assert.ErrorContains(t, err, "volume is in use") _, err = c.VolumeInspect(ctx, volName) assert.NilError(t, err) }) } t.Run("restartPolicy", func(t *testing.T) { runTest(t, "always") runTest(t, "unless-stopped") runTest(t, "on-failure") runTest(t, "no") }) // Make sure that the local volume driver's mount ref count is restored // Addresses https://github.com/moby/moby/issues/44422 t.Run("local volume with mount options", func(t *testing.T) { v, err := c.VolumeCreate(ctx, volume.CreateOptions{ Driver: "local", Name: "test-live-restore-volume-references-local", DriverOpts: map[string]string{ "type": "tmpfs", "device": "tmpfs", }, }) assert.NilError(t, err) m := mount.Mount{ Type: mount.TypeVolume, Source: v.Name, Target: "/foo", } const testContent = "hello" cID := container.Run(ctx, t, c, container.WithMount(m), container.WithCmd("sh", "-c", "echo "+testContent+">>/foo/test.txt; sleep infinity")) defer c.ContainerRemove(ctx, cID, types.ContainerRemoveOptions{Force: true}) // Wait until container creates a file in the volume. poll.WaitOn(t, func(t poll.LogT) poll.Result { stat, err := c.ContainerStatPath(ctx, cID, "/foo/test.txt") if err != nil { if errdefs.IsNotFound(err) { return poll.Continue("file doesn't yet exist") } return poll.Error(err) } if int(stat.Size) != len(testContent)+1 { return poll.Error(fmt.Errorf("unexpected test file size: %d", stat.Size)) } return poll.Success() }) d.Restart(t, "--live-restore", "--iptables=false") // Try to remove the volume // This should fail since its used by a container err = c.VolumeRemove(ctx, v.Name, false) assert.ErrorContains(t, err, "volume is in use") t.Run("volume still mounted", func(t *testing.T) { skip.If(t, testEnv.IsRootless(), "restarted rootless daemon has a new mount namespace and it won't have the previous mounts") // Check if a new container with the same volume has access to the previous content. // This fails if the volume gets unmounted at startup. cID2 := container.Run(ctx, t, c, container.WithMount(m), container.WithCmd("cat", "/foo/test.txt")) defer c.ContainerRemove(ctx, cID2, types.ContainerRemoveOptions{Force: true}) poll.WaitOn(t, container.IsStopped(ctx, c, cID2)) inspect, err := c.ContainerInspect(ctx, cID2) if assert.Check(t, err) { assert.Check(t, is.Equal(inspect.State.ExitCode, 0), "volume doesn't have the same file") } logs, err := c.ContainerLogs(ctx, cID2, types.ContainerLogsOptions{ShowStdout: true}) assert.NilError(t, err) defer logs.Close() var stdoutBuf bytes.Buffer _, err = stdcopy.StdCopy(&stdoutBuf, io.Discard, logs) assert.NilError(t, err) assert.Check(t, is.Equal(strings.TrimSpace(stdoutBuf.String()), testContent)) }) // Remove that container which should free the references in the volume err = c.ContainerRemove(ctx, cID, types.ContainerRemoveOptions{Force: true}) assert.NilError(t, err) // Now we should be able to remove the volume err = c.VolumeRemove(ctx, v.Name, false) assert.NilError(t, err) }) // Make sure that we don't panic if the container has bind-mounts // (which should not be "restored") // Regression test for https://github.com/moby/moby/issues/45898 t.Run("container with bind-mounts", func(t *testing.T) { m := mount.Mount{ Type: mount.TypeBind, Source: os.TempDir(), Target: "/foo", } cID := container.Run(ctx, t, c, container.WithMount(m), container.WithCmd("top")) defer c.ContainerRemove(ctx, cID, types.ContainerRemoveOptions{Force: true}) d.Restart(t, "--live-restore", "--iptables=false") err := c.ContainerRemove(ctx, cID, types.ContainerRemoveOptions{Force: true}) assert.NilError(t, err) }) } func TestDaemonDefaultBridgeWithFixedCidrButNoBip(t *testing.T) { skip.If(t, runtime.GOOS == "windows") bridgeName := "ext-bridge1" d := daemon.New(t, daemon.WithEnvVars("DOCKER_TEST_CREATE_DEFAULT_BRIDGE="+bridgeName)) defer func() { d.Stop(t) d.Cleanup(t) }() defer func() { // No need to clean up when running this test in rootless mode, as the // interface is deleted when the daemon is stopped and the netns // reclaimed by the kernel. if !testEnv.IsRootless() { deleteInterface(t, bridgeName) } }() d.StartWithBusybox(t, "--bridge", bridgeName, "--fixed-cidr", "192.168.130.0/24") } func deleteInterface(t *testing.T, ifName string) { icmd.RunCommand("ip", "link", "delete", ifName).Assert(t, icmd.Success) icmd.RunCommand("iptables", "-t", "nat", "--flush").Assert(t, icmd.Success) icmd.RunCommand("iptables", "--flush").Assert(t, icmd.Success) }