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 }