volume_test.go 11 KB

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