volume_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. package volume
  2. import (
  3. "net/http"
  4. "os"
  5. "path/filepath"
  6. "strings"
  7. "testing"
  8. "time"
  9. containertypes "github.com/docker/docker/api/types/container"
  10. "github.com/docker/docker/api/types/filters"
  11. "github.com/docker/docker/api/types/volume"
  12. clientpkg "github.com/docker/docker/client"
  13. "github.com/docker/docker/errdefs"
  14. "github.com/docker/docker/integration/internal/build"
  15. "github.com/docker/docker/integration/internal/container"
  16. "github.com/docker/docker/testutil"
  17. "github.com/docker/docker/testutil/daemon"
  18. "github.com/docker/docker/testutil/fakecontext"
  19. "github.com/docker/docker/testutil/request"
  20. "github.com/google/go-cmp/cmp/cmpopts"
  21. "gotest.tools/v3/assert"
  22. is "gotest.tools/v3/assert/cmp"
  23. "gotest.tools/v3/skip"
  24. )
  25. func TestVolumesCreateAndList(t *testing.T) {
  26. ctx := setupTest(t)
  27. client := testEnv.APIClient()
  28. name := t.Name()
  29. // Windows file system is case insensitive
  30. if testEnv.DaemonInfo.OSType == "windows" {
  31. name = strings.ToLower(name)
  32. }
  33. vol, err := client.VolumeCreate(ctx, volume.CreateOptions{
  34. Name: name,
  35. })
  36. assert.NilError(t, err)
  37. expected := volume.Volume{
  38. // Ignore timestamp of CreatedAt
  39. CreatedAt: vol.CreatedAt,
  40. Driver: "local",
  41. Scope: "local",
  42. Name: name,
  43. Mountpoint: filepath.Join(testEnv.DaemonInfo.DockerRootDir, "volumes", name, "_data"),
  44. }
  45. assert.Check(t, is.DeepEqual(vol, expected, cmpopts.EquateEmpty()))
  46. volList, err := client.VolumeList(ctx, volume.ListOptions{})
  47. assert.NilError(t, err)
  48. assert.Assert(t, len(volList.Volumes) > 0)
  49. volumes := volList.Volumes[:0]
  50. for _, v := range volList.Volumes {
  51. if v.Name == vol.Name {
  52. volumes = append(volumes, v)
  53. }
  54. }
  55. assert.Check(t, is.Equal(len(volumes), 1))
  56. assert.Check(t, volumes[0] != nil)
  57. assert.Check(t, is.DeepEqual(*volumes[0], expected, cmpopts.EquateEmpty()))
  58. }
  59. func TestVolumesRemove(t *testing.T) {
  60. ctx := setupTest(t)
  61. client := testEnv.APIClient()
  62. prefix, slash := getPrefixAndSlashFromDaemonPlatform()
  63. id := container.Create(ctx, t, client, container.WithVolume(prefix+slash+"foo"))
  64. c, err := client.ContainerInspect(ctx, id)
  65. assert.NilError(t, err)
  66. vname := c.Mounts[0].Name
  67. t.Run("volume in use", func(t *testing.T) {
  68. err = client.VolumeRemove(ctx, vname, false)
  69. assert.Check(t, is.ErrorType(err, errdefs.IsConflict))
  70. assert.Check(t, is.ErrorContains(err, "volume is in use"))
  71. })
  72. t.Run("volume not in use", func(t *testing.T) {
  73. err = client.ContainerRemove(ctx, id, containertypes.RemoveOptions{
  74. Force: true,
  75. })
  76. assert.NilError(t, err)
  77. err = client.VolumeRemove(ctx, vname, false)
  78. assert.NilError(t, err)
  79. })
  80. t.Run("non-existing volume", func(t *testing.T) {
  81. err = client.VolumeRemove(ctx, "no_such_volume", false)
  82. assert.Check(t, is.ErrorType(err, errdefs.IsNotFound))
  83. })
  84. t.Run("non-existing volume force", func(t *testing.T) {
  85. err = client.VolumeRemove(ctx, "no_such_volume", true)
  86. assert.NilError(t, err)
  87. })
  88. }
  89. // TestVolumesRemoveSwarmEnabled tests that an error is returned if a volume
  90. // is in use, also if swarm is enabled (and cluster volumes are supported).
  91. //
  92. // Regression test for https://github.com/docker/cli/issues/4082
  93. func TestVolumesRemoveSwarmEnabled(t *testing.T) {
  94. skip.If(t, testEnv.IsRemoteDaemon, "cannot run daemon when remote daemon")
  95. skip.If(t, testEnv.DaemonInfo.OSType == "windows", "TODO enable on windows")
  96. t.Parallel()
  97. ctx := setupTest(t)
  98. // Spin up a new daemon, so that we can run this test in parallel (it's a slow test)
  99. d := daemon.New(t)
  100. d.StartAndSwarmInit(ctx, t)
  101. defer d.Stop(t)
  102. client := d.NewClientT(t)
  103. prefix, slash := getPrefixAndSlashFromDaemonPlatform()
  104. id := container.Create(ctx, t, client, container.WithVolume(prefix+slash+"foo"))
  105. c, err := client.ContainerInspect(ctx, id)
  106. assert.NilError(t, err)
  107. vname := c.Mounts[0].Name
  108. t.Run("volume in use", func(t *testing.T) {
  109. err = client.VolumeRemove(ctx, vname, false)
  110. assert.Check(t, is.ErrorType(err, errdefs.IsConflict))
  111. assert.Check(t, is.ErrorContains(err, "volume is in use"))
  112. })
  113. t.Run("volume not in use", func(t *testing.T) {
  114. err = client.ContainerRemove(ctx, id, containertypes.RemoveOptions{
  115. Force: true,
  116. })
  117. assert.NilError(t, err)
  118. err = client.VolumeRemove(ctx, vname, false)
  119. assert.NilError(t, err)
  120. })
  121. t.Run("non-existing volume", func(t *testing.T) {
  122. err = client.VolumeRemove(ctx, "no_such_volume", false)
  123. assert.Check(t, is.ErrorType(err, errdefs.IsNotFound))
  124. })
  125. t.Run("non-existing volume force", func(t *testing.T) {
  126. err = client.VolumeRemove(ctx, "no_such_volume", true)
  127. assert.NilError(t, err)
  128. })
  129. }
  130. func TestVolumesInspect(t *testing.T) {
  131. ctx := setupTest(t)
  132. client := testEnv.APIClient()
  133. now := time.Now()
  134. vol, err := client.VolumeCreate(ctx, volume.CreateOptions{})
  135. assert.NilError(t, err)
  136. inspected, err := client.VolumeInspect(ctx, vol.Name)
  137. assert.NilError(t, err)
  138. assert.Check(t, is.DeepEqual(inspected, vol, cmpopts.EquateEmpty()))
  139. // comparing CreatedAt field time for the new volume to now. Truncate to 1 minute precision to avoid false positive
  140. createdAt, err := time.Parse(time.RFC3339, strings.TrimSpace(inspected.CreatedAt))
  141. assert.NilError(t, err)
  142. assert.Check(t, createdAt.Unix()-now.Unix() < 60, "CreatedAt (%s) exceeds creation time (%s) 60s", createdAt, now)
  143. // update atime and mtime for the "_data" directory (which would happen during volume initialization)
  144. modifiedAt := time.Now().Local().Add(5 * time.Hour)
  145. err = os.Chtimes(inspected.Mountpoint, modifiedAt, modifiedAt)
  146. assert.NilError(t, err)
  147. inspected, err = client.VolumeInspect(ctx, vol.Name)
  148. assert.NilError(t, err)
  149. createdAt2, err := time.Parse(time.RFC3339, strings.TrimSpace(inspected.CreatedAt))
  150. assert.NilError(t, err)
  151. // Check that CreatedAt didn't change after updating atime and mtime of the "_data" directory
  152. // Related issue: #38274
  153. assert.Equal(t, createdAt, createdAt2)
  154. }
  155. // TestVolumesInvalidJSON tests that POST endpoints that expect a body return
  156. // the correct error when sending invalid JSON requests.
  157. func TestVolumesInvalidJSON(t *testing.T) {
  158. ctx := setupTest(t)
  159. // POST endpoints that accept / expect a JSON body;
  160. endpoints := []string{"/volumes/create"}
  161. for _, ep := range endpoints {
  162. ep := ep
  163. t.Run(ep[1:], func(t *testing.T) {
  164. t.Parallel()
  165. ctx := testutil.StartSpan(ctx, t)
  166. t.Run("invalid content type", func(t *testing.T) {
  167. ctx := testutil.StartSpan(ctx, t)
  168. res, body, err := request.Post(ctx, ep, request.RawString("{}"), request.ContentType("text/plain"))
  169. assert.NilError(t, err)
  170. assert.Check(t, is.Equal(res.StatusCode, http.StatusBadRequest))
  171. buf, err := request.ReadBody(body)
  172. assert.NilError(t, err)
  173. assert.Check(t, is.Contains(string(buf), "unsupported Content-Type header (text/plain): must be 'application/json'"))
  174. })
  175. t.Run("invalid JSON", func(t *testing.T) {
  176. ctx := testutil.StartSpan(ctx, t)
  177. res, body, err := request.Post(ctx, ep, request.RawString("{invalid json"), request.JSON)
  178. assert.NilError(t, err)
  179. assert.Check(t, is.Equal(res.StatusCode, http.StatusBadRequest))
  180. buf, err := request.ReadBody(body)
  181. assert.NilError(t, err)
  182. assert.Check(t, is.Contains(string(buf), "invalid JSON: invalid character 'i' looking for beginning of object key string"))
  183. })
  184. t.Run("extra content after JSON", func(t *testing.T) {
  185. ctx := testutil.StartSpan(ctx, t)
  186. res, body, err := request.Post(ctx, ep, request.RawString(`{} trailing content`), request.JSON)
  187. assert.NilError(t, err)
  188. assert.Check(t, is.Equal(res.StatusCode, http.StatusBadRequest))
  189. buf, err := request.ReadBody(body)
  190. assert.NilError(t, err)
  191. assert.Check(t, is.Contains(string(buf), "unexpected content after JSON"))
  192. })
  193. t.Run("empty body", func(t *testing.T) {
  194. ctx := testutil.StartSpan(ctx, t)
  195. // empty body should not produce an 500 internal server error, or
  196. // any 5XX error (this is assuming the request does not produce
  197. // an internal server error for another reason, but it shouldn't)
  198. res, _, err := request.Post(ctx, ep, request.RawString(``), request.JSON)
  199. assert.NilError(t, err)
  200. assert.Check(t, res.StatusCode < http.StatusInternalServerError)
  201. })
  202. })
  203. }
  204. }
  205. func getPrefixAndSlashFromDaemonPlatform() (prefix, slash string) {
  206. if testEnv.DaemonInfo.OSType == "windows" {
  207. return "c:", `\`
  208. }
  209. return "", "/"
  210. }
  211. func TestVolumePruneAnonymous(t *testing.T) {
  212. ctx := setupTest(t)
  213. client := testEnv.APIClient()
  214. // Create an anonymous volume
  215. v, err := client.VolumeCreate(ctx, volume.CreateOptions{})
  216. assert.NilError(t, err)
  217. // Create a named volume
  218. vNamed, err := client.VolumeCreate(ctx, volume.CreateOptions{
  219. Name: "test",
  220. })
  221. assert.NilError(t, err)
  222. // Prune anonymous volumes
  223. pruneReport, err := client.VolumesPrune(ctx, filters.Args{})
  224. assert.NilError(t, err)
  225. assert.Check(t, is.Equal(len(pruneReport.VolumesDeleted), 1))
  226. assert.Check(t, is.Equal(pruneReport.VolumesDeleted[0], v.Name))
  227. _, err = client.VolumeInspect(ctx, vNamed.Name)
  228. assert.NilError(t, err)
  229. // Prune all volumes
  230. _, err = client.VolumeCreate(ctx, volume.CreateOptions{})
  231. assert.NilError(t, err)
  232. pruneReport, err = client.VolumesPrune(ctx, filters.NewArgs(filters.Arg("all", "1")))
  233. assert.NilError(t, err)
  234. assert.Check(t, is.Equal(len(pruneReport.VolumesDeleted), 2))
  235. // Validate that older API versions still have the old behavior of pruning all local volumes
  236. clientOld, err := clientpkg.NewClientWithOpts(clientpkg.FromEnv, clientpkg.WithVersion("1.41"))
  237. assert.NilError(t, err)
  238. defer clientOld.Close()
  239. assert.Equal(t, clientOld.ClientVersion(), "1.41")
  240. v, err = client.VolumeCreate(ctx, volume.CreateOptions{})
  241. assert.NilError(t, err)
  242. vNamed, err = client.VolumeCreate(ctx, volume.CreateOptions{Name: "test-api141"})
  243. assert.NilError(t, err)
  244. pruneReport, err = clientOld.VolumesPrune(ctx, filters.Args{})
  245. assert.NilError(t, err)
  246. assert.Check(t, is.Equal(len(pruneReport.VolumesDeleted), 2))
  247. assert.Check(t, is.Contains(pruneReport.VolumesDeleted, v.Name))
  248. assert.Check(t, is.Contains(pruneReport.VolumesDeleted, vNamed.Name))
  249. }
  250. func TestVolumePruneAnonFromImage(t *testing.T) {
  251. ctx := setupTest(t)
  252. client := testEnv.APIClient()
  253. volDest := "/foo"
  254. if testEnv.DaemonInfo.OSType == "windows" {
  255. volDest = `c:\\foo`
  256. }
  257. dockerfile := `FROM busybox
  258. VOLUME ` + volDest
  259. img := build.Do(ctx, t, client, fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile)))
  260. id := container.Create(ctx, t, client, container.WithImage(img))
  261. defer client.ContainerRemove(ctx, id, containertypes.RemoveOptions{})
  262. inspect, err := client.ContainerInspect(ctx, id)
  263. assert.NilError(t, err)
  264. assert.Assert(t, is.Len(inspect.Mounts, 1))
  265. volumeName := inspect.Mounts[0].Name
  266. assert.Assert(t, volumeName != "")
  267. err = client.ContainerRemove(ctx, id, containertypes.RemoveOptions{})
  268. assert.NilError(t, err)
  269. pruneReport, err := client.VolumesPrune(ctx, filters.Args{})
  270. assert.NilError(t, err)
  271. assert.Assert(t, is.Contains(pruneReport.VolumesDeleted, volumeName))
  272. }