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