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 }