daemon_test.go 15 KB

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