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