github.com/rawahars/moby@v24.0.4+incompatible/integration/daemon/daemon_test.go (about)

     1  package daemon // import "github.com/docker/docker/integration/daemon"
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"net/http/httptest"
     8  	"os"
     9  	"os/exec"
    10  	"path/filepath"
    11  	"runtime"
    12  	"strings"
    13  	"syscall"
    14  	"testing"
    15  
    16  	"github.com/docker/docker/api/types"
    17  	"github.com/docker/docker/api/types/mount"
    18  	"github.com/docker/docker/api/types/volume"
    19  	"github.com/docker/docker/daemon/config"
    20  	"github.com/docker/docker/integration/internal/container"
    21  	"github.com/docker/docker/testutil/daemon"
    22  	"gotest.tools/v3/assert"
    23  	is "gotest.tools/v3/assert/cmp"
    24  	"gotest.tools/v3/icmd"
    25  	"gotest.tools/v3/skip"
    26  )
    27  
    28  func TestConfigDaemonID(t *testing.T) {
    29  	skip.If(t, runtime.GOOS == "windows")
    30  
    31  	d := daemon.New(t)
    32  	defer d.Stop(t)
    33  
    34  	d.Start(t, "--iptables=false")
    35  	info := d.Info(t)
    36  	assert.Check(t, info.ID != "")
    37  	d.Stop(t)
    38  
    39  	// Verify that (if present) the engine-id file takes precedence
    40  	const engineID = "this-is-the-engine-id"
    41  	idFile := filepath.Join(d.RootDir(), "engine-id")
    42  	assert.Check(t, os.Remove(idFile))
    43  	// Using 0644 to allow rootless daemons to read the file (ideally
    44  	// we'd chown the file to have the remapped user as owner).
    45  	err := os.WriteFile(idFile, []byte(engineID), 0o644)
    46  	assert.NilError(t, err)
    47  
    48  	d.Start(t, "--iptables=false")
    49  	info = d.Info(t)
    50  	assert.Equal(t, info.ID, engineID)
    51  	d.Stop(t)
    52  }
    53  
    54  func TestDaemonConfigValidation(t *testing.T) {
    55  	skip.If(t, runtime.GOOS == "windows")
    56  
    57  	d := daemon.New(t)
    58  	dockerBinary, err := d.BinaryPath()
    59  	assert.NilError(t, err)
    60  	params := []string{"--validate", "--config-file"}
    61  
    62  	dest := os.Getenv("DOCKER_INTEGRATION_DAEMON_DEST")
    63  	if dest == "" {
    64  		dest = os.Getenv("DEST")
    65  	}
    66  	testdata := filepath.Join(dest, "..", "..", "integration", "daemon", "testdata")
    67  
    68  	const (
    69  		validOut  = "configuration OK"
    70  		failedOut = "unable to configure the Docker daemon with file"
    71  	)
    72  
    73  	tests := []struct {
    74  		name        string
    75  		args        []string
    76  		expectedOut string
    77  	}{
    78  		{
    79  			name:        "config with no content",
    80  			args:        append(params, filepath.Join(testdata, "empty-config-1.json")),
    81  			expectedOut: validOut,
    82  		},
    83  		{
    84  			name:        "config with {}",
    85  			args:        append(params, filepath.Join(testdata, "empty-config-2.json")),
    86  			expectedOut: validOut,
    87  		},
    88  		{
    89  			name:        "invalid config",
    90  			args:        append(params, filepath.Join(testdata, "invalid-config-1.json")),
    91  			expectedOut: failedOut,
    92  		},
    93  		{
    94  			name:        "malformed config",
    95  			args:        append(params, filepath.Join(testdata, "malformed-config.json")),
    96  			expectedOut: failedOut,
    97  		},
    98  		{
    99  			name:        "valid config",
   100  			args:        append(params, filepath.Join(testdata, "valid-config-1.json")),
   101  			expectedOut: validOut,
   102  		},
   103  	}
   104  	for _, tc := range tests {
   105  		tc := tc
   106  		t.Run(tc.name, func(t *testing.T) {
   107  			t.Parallel()
   108  			cmd := exec.Command(dockerBinary, tc.args...)
   109  			out, err := cmd.CombinedOutput()
   110  			assert.Check(t, is.Contains(string(out), tc.expectedOut))
   111  			if tc.expectedOut == failedOut {
   112  				assert.ErrorContains(t, err, "", "expected an error, but got none")
   113  			} else {
   114  				assert.NilError(t, err)
   115  			}
   116  		})
   117  	}
   118  }
   119  
   120  func TestConfigDaemonSeccompProfiles(t *testing.T) {
   121  	skip.If(t, runtime.GOOS == "windows")
   122  
   123  	d := daemon.New(t)
   124  	defer d.Stop(t)
   125  
   126  	tests := []struct {
   127  		doc             string
   128  		profile         string
   129  		expectedProfile string
   130  	}{
   131  		{
   132  			doc:             "empty profile set",
   133  			profile:         "",
   134  			expectedProfile: config.SeccompProfileDefault,
   135  		},
   136  		{
   137  			doc:             "default profile",
   138  			profile:         config.SeccompProfileDefault,
   139  			expectedProfile: config.SeccompProfileDefault,
   140  		},
   141  		{
   142  			doc:             "unconfined profile",
   143  			profile:         config.SeccompProfileUnconfined,
   144  			expectedProfile: config.SeccompProfileUnconfined,
   145  		},
   146  	}
   147  
   148  	for _, tc := range tests {
   149  		tc := tc
   150  		t.Run(tc.doc, func(t *testing.T) {
   151  			d.Start(t, "--seccomp-profile="+tc.profile)
   152  			info := d.Info(t)
   153  			assert.Assert(t, is.Contains(info.SecurityOptions, "name=seccomp,profile="+tc.expectedProfile))
   154  			d.Stop(t)
   155  
   156  			cfg := filepath.Join(d.RootDir(), "daemon.json")
   157  			err := os.WriteFile(cfg, []byte(`{"seccomp-profile": "`+tc.profile+`"}`), 0644)
   158  			assert.NilError(t, err)
   159  
   160  			d.Start(t, "--config-file", cfg)
   161  			info = d.Info(t)
   162  			assert.Assert(t, is.Contains(info.SecurityOptions, "name=seccomp,profile="+tc.expectedProfile))
   163  			d.Stop(t)
   164  		})
   165  	}
   166  }
   167  
   168  func TestDaemonProxy(t *testing.T) {
   169  	skip.If(t, runtime.GOOS == "windows", "cannot start multiple daemons on windows")
   170  	skip.If(t, os.Getenv("DOCKER_ROOTLESS") != "", "cannot connect to localhost proxy in rootless environment")
   171  
   172  	var received string
   173  	proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   174  		received = r.Host
   175  		w.Header().Set("Content-Type", "application/json")
   176  		_, _ = w.Write([]byte("OK"))
   177  	}))
   178  	defer proxyServer.Close()
   179  
   180  	const userPass = "myuser:mypassword@"
   181  
   182  	// Configure proxy through env-vars
   183  	t.Run("environment variables", func(t *testing.T) {
   184  		t.Setenv("HTTP_PROXY", proxyServer.URL)
   185  		t.Setenv("HTTPS_PROXY", proxyServer.URL)
   186  		t.Setenv("NO_PROXY", "example.com")
   187  
   188  		d := daemon.New(t)
   189  		c := d.NewClientT(t)
   190  		defer func() { _ = c.Close() }()
   191  		ctx := context.Background()
   192  		d.Start(t)
   193  
   194  		_, err := c.ImagePull(ctx, "example.org:5000/some/image:latest", types.ImagePullOptions{})
   195  		assert.ErrorContains(t, err, "", "pulling should have failed")
   196  		assert.Equal(t, received, "example.org:5000")
   197  
   198  		// Test NoProxy: example.com should not hit the proxy, and "received" variable should not be changed.
   199  		_, err = c.ImagePull(ctx, "example.com/some/image:latest", types.ImagePullOptions{})
   200  		assert.ErrorContains(t, err, "", "pulling should have failed")
   201  		assert.Equal(t, received, "example.org:5000", "should not have used proxy")
   202  
   203  		info := d.Info(t)
   204  		assert.Equal(t, info.HTTPProxy, proxyServer.URL)
   205  		assert.Equal(t, info.HTTPSProxy, proxyServer.URL)
   206  		assert.Equal(t, info.NoProxy, "example.com")
   207  		d.Stop(t)
   208  	})
   209  
   210  	// Configure proxy through command-line flags
   211  	t.Run("command-line options", func(t *testing.T) {
   212  		t.Setenv("HTTP_PROXY", "http://"+userPass+"from-env-http.invalid")
   213  		t.Setenv("http_proxy", "http://"+userPass+"from-env-http.invalid")
   214  		t.Setenv("HTTPS_PROXY", "https://"+userPass+"myuser:mypassword@from-env-https.invalid")
   215  		t.Setenv("https_proxy", "https://"+userPass+"myuser:mypassword@from-env-https.invalid")
   216  		t.Setenv("NO_PROXY", "ignore.invalid")
   217  		t.Setenv("no_proxy", "ignore.invalid")
   218  
   219  		d := daemon.New(t)
   220  		d.Start(t, "--http-proxy", proxyServer.URL, "--https-proxy", proxyServer.URL, "--no-proxy", "example.com")
   221  
   222  		logs, err := d.ReadLogFile()
   223  		assert.NilError(t, err)
   224  		assert.Assert(t, is.Contains(string(logs), "overriding existing proxy variable with value from configuration"))
   225  		for _, v := range []string{"http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY", "no_proxy", "NO_PROXY"} {
   226  			assert.Assert(t, is.Contains(string(logs), "name="+v))
   227  			assert.Assert(t, !strings.Contains(string(logs), userPass), "logs should not contain the non-sanitized proxy URL: %s", string(logs))
   228  		}
   229  
   230  		c := d.NewClientT(t)
   231  		defer func() { _ = c.Close() }()
   232  		ctx := context.Background()
   233  
   234  		_, err = c.ImagePull(ctx, "example.org:5001/some/image:latest", types.ImagePullOptions{})
   235  		assert.ErrorContains(t, err, "", "pulling should have failed")
   236  		assert.Equal(t, received, "example.org:5001")
   237  
   238  		// Test NoProxy: example.com should not hit the proxy, and "received" variable should not be changed.
   239  		_, err = c.ImagePull(ctx, "example.com/some/image:latest", types.ImagePullOptions{})
   240  		assert.ErrorContains(t, err, "", "pulling should have failed")
   241  		assert.Equal(t, received, "example.org:5001", "should not have used proxy")
   242  
   243  		info := d.Info(t)
   244  		assert.Equal(t, info.HTTPProxy, proxyServer.URL)
   245  		assert.Equal(t, info.HTTPSProxy, proxyServer.URL)
   246  		assert.Equal(t, info.NoProxy, "example.com")
   247  
   248  		d.Stop(t)
   249  	})
   250  
   251  	// Configure proxy through configuration file
   252  	t.Run("configuration file", func(t *testing.T) {
   253  		t.Setenv("HTTP_PROXY", "http://"+userPass+"from-env-http.invalid")
   254  		t.Setenv("http_proxy", "http://"+userPass+"from-env-http.invalid")
   255  		t.Setenv("HTTPS_PROXY", "https://"+userPass+"myuser:mypassword@from-env-https.invalid")
   256  		t.Setenv("https_proxy", "https://"+userPass+"myuser:mypassword@from-env-https.invalid")
   257  		t.Setenv("NO_PROXY", "ignore.invalid")
   258  		t.Setenv("no_proxy", "ignore.invalid")
   259  
   260  		d := daemon.New(t)
   261  		c := d.NewClientT(t)
   262  		defer func() { _ = c.Close() }()
   263  		ctx := context.Background()
   264  
   265  		configFile := filepath.Join(d.RootDir(), "daemon.json")
   266  		configJSON := fmt.Sprintf(`{"proxies":{"http-proxy":%[1]q, "https-proxy": %[1]q, "no-proxy": "example.com"}}`, proxyServer.URL)
   267  		assert.NilError(t, os.WriteFile(configFile, []byte(configJSON), 0644))
   268  
   269  		d.Start(t, "--config-file", configFile)
   270  
   271  		logs, err := d.ReadLogFile()
   272  		assert.NilError(t, err)
   273  		assert.Assert(t, is.Contains(string(logs), "overriding existing proxy variable with value from configuration"))
   274  		for _, v := range []string{"http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY", "no_proxy", "NO_PROXY"} {
   275  			assert.Assert(t, is.Contains(string(logs), "name="+v))
   276  			assert.Assert(t, !strings.Contains(string(logs), userPass), "logs should not contain the non-sanitized proxy URL: %s", string(logs))
   277  		}
   278  
   279  		_, err = c.ImagePull(ctx, "example.org:5002/some/image:latest", types.ImagePullOptions{})
   280  		assert.ErrorContains(t, err, "", "pulling should have failed")
   281  		assert.Equal(t, received, "example.org:5002")
   282  
   283  		// Test NoProxy: example.com should not hit the proxy, and "received" variable should not be changed.
   284  		_, err = c.ImagePull(ctx, "example.com/some/image:latest", types.ImagePullOptions{})
   285  		assert.ErrorContains(t, err, "", "pulling should have failed")
   286  		assert.Equal(t, received, "example.org:5002", "should not have used proxy")
   287  
   288  		info := d.Info(t)
   289  		assert.Equal(t, info.HTTPProxy, proxyServer.URL)
   290  		assert.Equal(t, info.HTTPSProxy, proxyServer.URL)
   291  		assert.Equal(t, info.NoProxy, "example.com")
   292  
   293  		d.Stop(t)
   294  	})
   295  
   296  	// Conflicting options (passed both through command-line options and config file)
   297  	t.Run("conflicting options", func(t *testing.T) {
   298  		const (
   299  			proxyRawURL = "https://" + userPass + "example.org"
   300  			proxyURL    = "https://xxxxx:xxxxx@example.org"
   301  		)
   302  
   303  		d := daemon.New(t)
   304  
   305  		configFile := filepath.Join(d.RootDir(), "daemon.json")
   306  		configJSON := fmt.Sprintf(`{"proxies":{"http-proxy":%[1]q, "https-proxy": %[1]q, "no-proxy": "example.com"}}`, proxyRawURL)
   307  		assert.NilError(t, os.WriteFile(configFile, []byte(configJSON), 0644))
   308  
   309  		err := d.StartWithError("--http-proxy", proxyRawURL, "--https-proxy", proxyRawURL, "--no-proxy", "example.com", "--config-file", configFile, "--validate")
   310  		assert.ErrorContains(t, err, "daemon exited during startup")
   311  		logs, err := d.ReadLogFile()
   312  		assert.NilError(t, err)
   313  		expected := fmt.Sprintf(
   314  			`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)`,
   315  			proxyURL,
   316  		)
   317  		assert.Assert(t, is.Contains(string(logs), expected))
   318  	})
   319  
   320  	// Make sure values are sanitized when reloading the daemon-config
   321  	t.Run("reload sanitized", func(t *testing.T) {
   322  		const (
   323  			proxyRawURL = "https://" + userPass + "example.org"
   324  			proxyURL    = "https://xxxxx:xxxxx@example.org"
   325  		)
   326  
   327  		d := daemon.New(t)
   328  		d.Start(t, "--http-proxy", proxyRawURL, "--https-proxy", proxyRawURL, "--no-proxy", "example.com")
   329  		defer d.Stop(t)
   330  		err := d.Signal(syscall.SIGHUP)
   331  		assert.NilError(t, err)
   332  
   333  		logs, err := d.ReadLogFile()
   334  		assert.NilError(t, err)
   335  
   336  		// FIXME: there appears to ba a race condition, which causes ReadLogFile
   337  		//        to not contain the full logs after signaling the daemon to reload,
   338  		//        causing the test to fail here. As a workaround, check if we
   339  		//        received the "reloaded" message after signaling, and only then
   340  		//        check that it's sanitized properly. For more details on this
   341  		//        issue, see https://github.com/moby/moby/pull/42835/files#r713120315
   342  		if !strings.Contains(string(logs), "Reloaded configuration:") {
   343  			t.Skip("Skipping test, because we did not find 'Reloaded configuration' in the logs")
   344  		}
   345  
   346  		assert.Assert(t, is.Contains(string(logs), proxyURL))
   347  		assert.Assert(t, !strings.Contains(string(logs), userPass), "logs should not contain the non-sanitized proxy URL: %s", string(logs))
   348  	})
   349  }
   350  
   351  func TestLiveRestore(t *testing.T) {
   352  	skip.If(t, runtime.GOOS == "windows", "cannot start multiple daemons on windows")
   353  
   354  	t.Run("volume references", testLiveRestoreVolumeReferences)
   355  }
   356  
   357  func testLiveRestoreVolumeReferences(t *testing.T) {
   358  	t.Parallel()
   359  
   360  	d := daemon.New(t)
   361  	d.StartWithBusybox(t, "--live-restore", "--iptables=false")
   362  	defer func() {
   363  		d.Stop(t)
   364  		d.Cleanup(t)
   365  	}()
   366  
   367  	c := d.NewClientT(t)
   368  	ctx := context.Background()
   369  
   370  	runTest := func(t *testing.T, policy string) {
   371  		t.Run(policy, func(t *testing.T) {
   372  			volName := "test-live-restore-volume-references-" + policy
   373  			_, err := c.VolumeCreate(ctx, volume.CreateOptions{Name: volName})
   374  			assert.NilError(t, err)
   375  
   376  			// Create a container that uses the volume
   377  			m := mount.Mount{
   378  				Type:   mount.TypeVolume,
   379  				Source: volName,
   380  				Target: "/foo",
   381  			}
   382  			cID := container.Run(ctx, t, c, container.WithMount(m), container.WithCmd("top"), container.WithRestartPolicy(policy))
   383  			defer c.ContainerRemove(ctx, cID, types.ContainerRemoveOptions{Force: true})
   384  
   385  			// Stop the daemon
   386  			d.Restart(t, "--live-restore", "--iptables=false")
   387  
   388  			// Try to remove the volume
   389  			err = c.VolumeRemove(ctx, volName, false)
   390  			assert.ErrorContains(t, err, "volume is in use")
   391  
   392  			_, err = c.VolumeInspect(ctx, volName)
   393  			assert.NilError(t, err)
   394  		})
   395  	}
   396  
   397  	t.Run("restartPolicy", func(t *testing.T) {
   398  		runTest(t, "always")
   399  		runTest(t, "unless-stopped")
   400  		runTest(t, "on-failure")
   401  		runTest(t, "no")
   402  	})
   403  
   404  	// Make sure that the local volume driver's mount ref count is restored
   405  	// Addresses https://github.com/moby/moby/issues/44422
   406  	t.Run("local volume with mount options", func(t *testing.T) {
   407  		v, err := c.VolumeCreate(ctx, volume.CreateOptions{
   408  			Driver: "local",
   409  			Name:   "test-live-restore-volume-references-local",
   410  			DriverOpts: map[string]string{
   411  				"type":   "tmpfs",
   412  				"device": "tmpfs",
   413  			},
   414  		})
   415  		assert.NilError(t, err)
   416  		m := mount.Mount{
   417  			Type:   mount.TypeVolume,
   418  			Source: v.Name,
   419  			Target: "/foo",
   420  		}
   421  		cID := container.Run(ctx, t, c, container.WithMount(m), container.WithCmd("top"))
   422  		defer c.ContainerRemove(ctx, cID, types.ContainerRemoveOptions{Force: true})
   423  
   424  		d.Restart(t, "--live-restore", "--iptables=false")
   425  
   426  		// Try to remove the volume
   427  		// This should fail since its used by a container
   428  		err = c.VolumeRemove(ctx, v.Name, false)
   429  		assert.ErrorContains(t, err, "volume is in use")
   430  
   431  		// Remove that container which should free the references in the volume
   432  		err = c.ContainerRemove(ctx, cID, types.ContainerRemoveOptions{Force: true})
   433  		assert.NilError(t, err)
   434  
   435  		// Now we should be able to remove the volume
   436  		err = c.VolumeRemove(ctx, v.Name, false)
   437  		assert.NilError(t, err)
   438  	})
   439  
   440  	// Make sure that we don't panic if the container has bind-mounts
   441  	// (which should not be "restored")
   442  	// Regression test for https://github.com/moby/moby/issues/45898
   443  	t.Run("container with bind-mounts", func(t *testing.T) {
   444  		m := mount.Mount{
   445  			Type:   mount.TypeBind,
   446  			Source: os.TempDir(),
   447  			Target: "/foo",
   448  		}
   449  		cID := container.Run(ctx, t, c, container.WithMount(m), container.WithCmd("top"))
   450  		defer c.ContainerRemove(ctx, cID, types.ContainerRemoveOptions{Force: true})
   451  
   452  		d.Restart(t, "--live-restore", "--iptables=false")
   453  
   454  		err := c.ContainerRemove(ctx, cID, types.ContainerRemoveOptions{Force: true})
   455  		assert.NilError(t, err)
   456  	})
   457  }
   458  
   459  func TestDaemonDefaultBridgeWithFixedCidrButNoBip(t *testing.T) {
   460  	skip.If(t, runtime.GOOS == "windows")
   461  
   462  	bridgeName := "ext-bridge1"
   463  	d := daemon.New(t, daemon.WithEnvVars("DOCKER_TEST_CREATE_DEFAULT_BRIDGE="+bridgeName))
   464  	defer func() {
   465  		d.Stop(t)
   466  		d.Cleanup(t)
   467  	}()
   468  
   469  	defer func() {
   470  		// No need to clean up when running this test in rootless mode, as the
   471  		// interface is deleted when the daemon is stopped and the netns
   472  		// reclaimed by the kernel.
   473  		if !testEnv.IsRootless() {
   474  			deleteInterface(t, bridgeName)
   475  		}
   476  	}()
   477  	d.StartWithBusybox(t, "--bridge", bridgeName, "--fixed-cidr", "192.168.130.0/24")
   478  }
   479  
   480  func deleteInterface(t *testing.T, ifName string) {
   481  	icmd.RunCommand("ip", "link", "delete", ifName).Assert(t, icmd.Success)
   482  	icmd.RunCommand("iptables", "-t", "nat", "--flush").Assert(t, icmd.Success)
   483  	icmd.RunCommand("iptables", "--flush").Assert(t, icmd.Success)
   484  }