daemon_test.go 13 KB

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