| package daemon // import "github.com/docker/docker/integration/daemon" |
| |
| import ( |
| "context" |
| "fmt" |
| "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/integration/internal/container" |
| "github.com/docker/docker/testutil/daemon" |
| "gotest.tools/v3/assert" |
| is "gotest.tools/v3/assert/cmp" |
| "gotest.tools/v3/icmd" |
| "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") |
| |
| var received string |
| proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| received = r.Host |
| w.Header().Set("Content-Type", "application/json") |
| _, _ = w.Write([]byte("OK")) |
| })) |
| defer proxyServer.Close() |
| |
| const userPass = "myuser:mypassword@" |
| |
| // Configure proxy through env-vars |
| t.Run("environment variables", func(t *testing.T) { |
| t.Setenv("HTTP_PROXY", proxyServer.URL) |
| t.Setenv("HTTPS_PROXY", proxyServer.URL) |
| t.Setenv("NO_PROXY", "example.com") |
| |
| d := daemon.New(t) |
| c := d.NewClientT(t) |
| defer func() { _ = c.Close() }() |
| ctx := context.Background() |
| d.Start(t) |
| |
| _, 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") |
| |
| info := d.Info(t) |
| assert.Equal(t, info.HTTPProxy, proxyServer.URL) |
| assert.Equal(t, info.HTTPSProxy, proxyServer.URL) |
| assert.Equal(t, info.NoProxy, "example.com") |
| d.Stop(t) |
| }) |
| |
| // Configure proxy through command-line flags |
| t.Run("command-line options", func(t *testing.T) { |
| t.Setenv("HTTP_PROXY", "http://"+userPass+"from-env-http.invalid") |
| t.Setenv("http_proxy", "http://"+userPass+"from-env-http.invalid") |
| t.Setenv("HTTPS_PROXY", "https://"+userPass+"myuser:mypassword@from-env-https.invalid") |
| t.Setenv("https_proxy", "https://"+userPass+"myuser:mypassword@from-env-https.invalid") |
| t.Setenv("NO_PROXY", "ignore.invalid") |
| t.Setenv("no_proxy", "ignore.invalid") |
| |
| d := daemon.New(t) |
| d.Start(t, "--http-proxy", proxyServer.URL, "--https-proxy", proxyServer.URL, "--no-proxy", "example.com") |
| |
| logs, err := d.ReadLogFile() |
| assert.NilError(t, err) |
| assert.Assert(t, is.Contains(string(logs), "overriding existing proxy variable with value from configuration")) |
| for _, v := range []string{"http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY", "no_proxy", "NO_PROXY"} { |
| assert.Assert(t, is.Contains(string(logs), "name="+v)) |
| assert.Assert(t, !strings.Contains(string(logs), userPass), "logs should not contain the non-sanitized proxy URL: %s", string(logs)) |
| } |
| |
| c := d.NewClientT(t) |
| defer func() { _ = c.Close() }() |
| ctx := context.Background() |
| |
| _, 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") |
| |
| info := d.Info(t) |
| assert.Equal(t, info.HTTPProxy, proxyServer.URL) |
| assert.Equal(t, info.HTTPSProxy, proxyServer.URL) |
| assert.Equal(t, info.NoProxy, "example.com") |
| |
| d.Stop(t) |
| }) |
| |
| // Configure proxy through configuration file |
| t.Run("configuration file", func(t *testing.T) { |
| t.Setenv("HTTP_PROXY", "http://"+userPass+"from-env-http.invalid") |
| t.Setenv("http_proxy", "http://"+userPass+"from-env-http.invalid") |
| t.Setenv("HTTPS_PROXY", "https://"+userPass+"myuser:mypassword@from-env-https.invalid") |
| t.Setenv("https_proxy", "https://"+userPass+"myuser:mypassword@from-env-https.invalid") |
| t.Setenv("NO_PROXY", "ignore.invalid") |
| t.Setenv("no_proxy", "ignore.invalid") |
| |
| d := daemon.New(t) |
| c := d.NewClientT(t) |
| defer func() { _ = c.Close() }() |
| ctx := context.Background() |
| |
| 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, "--config-file", configFile) |
| |
| logs, err := d.ReadLogFile() |
| assert.NilError(t, err) |
| assert.Assert(t, is.Contains(string(logs), "overriding existing proxy variable with value from configuration")) |
| for _, v := range []string{"http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY", "no_proxy", "NO_PROXY"} { |
| assert.Assert(t, is.Contains(string(logs), "name="+v)) |
| assert.Assert(t, !strings.Contains(string(logs), userPass), "logs should not contain the non-sanitized proxy URL: %s", string(logs)) |
| } |
| |
| _, 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") |
| |
| info := d.Info(t) |
| assert.Equal(t, info.HTTPProxy, proxyServer.URL) |
| assert.Equal(t, info.HTTPSProxy, proxyServer.URL) |
| assert.Equal(t, info.NoProxy, "example.com") |
| |
| d.Stop(t) |
| }) |
| |
| // Conflicting options (passed both through command-line options and config file) |
| t.Run("conflicting options", func(t *testing.T) { |
| 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") |
| logs, err := d.ReadLogFile() |
| assert.NilError(t, err) |
| 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, |
| ) |
| assert.Assert(t, is.Contains(string(logs), expected)) |
| }) |
| |
| // Make sure values are sanitized when reloading the daemon-config |
| t.Run("reload sanitized", func(t *testing.T) { |
| const ( |
| proxyRawURL = "https://" + userPass + "example.org" |
| proxyURL = "https://xxxxx:xxxxx@example.org" |
| ) |
| |
| d := daemon.New(t) |
| d.Start(t, "--http-proxy", proxyRawURL, "--https-proxy", proxyRawURL, "--no-proxy", "example.com") |
| defer d.Stop(t) |
| err := d.Signal(syscall.SIGHUP) |
| assert.NilError(t, err) |
| |
| logs, err := d.ReadLogFile() |
| assert.NilError(t, err) |
| |
| // FIXME: there appears to ba a race condition, which causes ReadLogFile |
| // to not contain the full logs after signaling the daemon to reload, |
| // causing the test to fail here. As a workaround, check if we |
| // received the "reloaded" message after signaling, and only then |
| // check that it's sanitized properly. For more details on this |
| // issue, see https://github.com/moby/moby/pull/42835/files#r713120315 |
| if !strings.Contains(string(logs), "Reloaded configuration:") { |
| t.Skip("Skipping test, because we did not find 'Reloaded configuration' in the logs") |
| } |
| |
| assert.Assert(t, is.Contains(string(logs), proxyURL)) |
| assert.Assert(t, !strings.Contains(string(logs), userPass), "logs should not contain the non-sanitized proxy URL: %s", string(logs)) |
| }) |
| } |
| |
| func TestLiveRestore(t *testing.T) { |
| skip.If(t, runtime.GOOS == "windows", "cannot start multiple daemons on windows") |
| |
| t.Run("volume references", testLiveRestoreVolumeReferences) |
| } |
| |
| 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") |
| }) |
| } |
| |
| 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) |
| } |