daemon_test.go 16 KB

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