volume_test.go 11 KB

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