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