github.com/Prakhar-Agarwal-byte/moby@v0.0.0-20231027092010-a14e3e8ab87e/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/Prakhar-Agarwal-byte/moby/api/types/container" 12 "github.com/Prakhar-Agarwal-byte/moby/api/types/filters" 13 "github.com/Prakhar-Agarwal-byte/moby/api/types/volume" 14 clientpkg "github.com/Prakhar-Agarwal-byte/moby/client" 15 "github.com/Prakhar-Agarwal-byte/moby/errdefs" 16 "github.com/Prakhar-Agarwal-byte/moby/integration/internal/build" 17 "github.com/Prakhar-Agarwal-byte/moby/integration/internal/container" 18 "github.com/Prakhar-Agarwal-byte/moby/testutil" 19 "github.com/Prakhar-Agarwal-byte/moby/testutil/daemon" 20 "github.com/Prakhar-Agarwal-byte/moby/testutil/fakecontext" 21 "github.com/Prakhar-Agarwal-byte/moby/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 t.Parallel() 115 ctx := setupTest(t) 116 117 // Spin up a new daemon, so that we can run this test in parallel (it's a slow test) 118 d := daemon.New(t) 119 d.StartAndSwarmInit(ctx, t) 120 defer d.Stop(t) 121 122 client := d.NewClientT(t) 123 124 prefix, slash := getPrefixAndSlashFromDaemonPlatform() 125 id := container.Create(ctx, t, client, container.WithVolume(prefix+slash+"foo")) 126 127 c, err := client.ContainerInspect(ctx, id) 128 assert.NilError(t, err) 129 vname := c.Mounts[0].Name 130 131 t.Run("volume in use", func(t *testing.T) { 132 err = client.VolumeRemove(ctx, vname, false) 133 assert.Check(t, is.ErrorType(err, errdefs.IsConflict)) 134 assert.Check(t, is.ErrorContains(err, "volume is in use")) 135 }) 136 137 t.Run("volume not in use", func(t *testing.T) { 138 err = client.ContainerRemove(ctx, id, containertypes.RemoveOptions{ 139 Force: true, 140 }) 141 assert.NilError(t, err) 142 143 err = client.VolumeRemove(ctx, vname, false) 144 assert.NilError(t, err) 145 }) 146 147 t.Run("non-existing volume", func(t *testing.T) { 148 err = client.VolumeRemove(ctx, "no_such_volume", false) 149 assert.Check(t, is.ErrorType(err, errdefs.IsNotFound)) 150 }) 151 152 t.Run("non-existing volume force", func(t *testing.T) { 153 err = client.VolumeRemove(ctx, "no_such_volume", true) 154 assert.NilError(t, err) 155 }) 156 } 157 158 func TestVolumesInspect(t *testing.T) { 159 ctx := setupTest(t) 160 client := testEnv.APIClient() 161 162 now := time.Now() 163 vol, err := client.VolumeCreate(ctx, volume.CreateOptions{}) 164 assert.NilError(t, err) 165 166 inspected, err := client.VolumeInspect(ctx, vol.Name) 167 assert.NilError(t, err) 168 169 assert.Check(t, is.DeepEqual(inspected, vol, cmpopts.EquateEmpty())) 170 171 // comparing CreatedAt field time for the new volume to now. Truncate to 1 minute precision to avoid false positive 172 createdAt, err := time.Parse(time.RFC3339, strings.TrimSpace(inspected.CreatedAt)) 173 assert.NilError(t, err) 174 assert.Check(t, createdAt.Unix()-now.Unix() < 60, "CreatedAt (%s) exceeds creation time (%s) 60s", createdAt, now) 175 176 // update atime and mtime for the "_data" directory (which would happen during volume initialization) 177 modifiedAt := time.Now().Local().Add(5 * time.Hour) 178 err = os.Chtimes(inspected.Mountpoint, modifiedAt, modifiedAt) 179 assert.NilError(t, err) 180 181 inspected, err = client.VolumeInspect(ctx, vol.Name) 182 assert.NilError(t, err) 183 184 createdAt2, err := time.Parse(time.RFC3339, strings.TrimSpace(inspected.CreatedAt)) 185 assert.NilError(t, err) 186 187 // Check that CreatedAt didn't change after updating atime and mtime of the "_data" directory 188 // Related issue: #38274 189 assert.Equal(t, createdAt, createdAt2) 190 } 191 192 // TestVolumesInvalidJSON tests that POST endpoints that expect a body return 193 // the correct error when sending invalid JSON requests. 194 func TestVolumesInvalidJSON(t *testing.T) { 195 ctx := setupTest(t) 196 197 // POST endpoints that accept / expect a JSON body; 198 endpoints := []string{"/volumes/create"} 199 200 for _, ep := range endpoints { 201 ep := ep 202 t.Run(ep[1:], func(t *testing.T) { 203 t.Parallel() 204 ctx := testutil.StartSpan(ctx, t) 205 206 t.Run("invalid content type", func(t *testing.T) { 207 ctx := testutil.StartSpan(ctx, t) 208 res, body, err := request.Post(ctx, ep, request.RawString("{}"), request.ContentType("text/plain")) 209 assert.NilError(t, err) 210 assert.Check(t, is.Equal(res.StatusCode, http.StatusBadRequest)) 211 212 buf, err := request.ReadBody(body) 213 assert.NilError(t, err) 214 assert.Check(t, is.Contains(string(buf), "unsupported Content-Type header (text/plain): must be 'application/json'")) 215 }) 216 217 t.Run("invalid JSON", func(t *testing.T) { 218 ctx := testutil.StartSpan(ctx, t) 219 res, body, err := request.Post(ctx, ep, request.RawString("{invalid json"), request.JSON) 220 assert.NilError(t, err) 221 assert.Check(t, is.Equal(res.StatusCode, http.StatusBadRequest)) 222 223 buf, err := request.ReadBody(body) 224 assert.NilError(t, err) 225 assert.Check(t, is.Contains(string(buf), "invalid JSON: invalid character 'i' looking for beginning of object key string")) 226 }) 227 228 t.Run("extra content after JSON", func(t *testing.T) { 229 ctx := testutil.StartSpan(ctx, t) 230 res, body, err := request.Post(ctx, ep, request.RawString(`{} trailing content`), request.JSON) 231 assert.NilError(t, err) 232 assert.Check(t, is.Equal(res.StatusCode, http.StatusBadRequest)) 233 234 buf, err := request.ReadBody(body) 235 assert.NilError(t, err) 236 assert.Check(t, is.Contains(string(buf), "unexpected content after JSON")) 237 }) 238 239 t.Run("empty body", func(t *testing.T) { 240 ctx := testutil.StartSpan(ctx, t) 241 // empty body should not produce an 500 internal server error, or 242 // any 5XX error (this is assuming the request does not produce 243 // an internal server error for another reason, but it shouldn't) 244 res, _, err := request.Post(ctx, ep, request.RawString(``), request.JSON) 245 assert.NilError(t, err) 246 assert.Check(t, res.StatusCode < http.StatusInternalServerError) 247 }) 248 }) 249 } 250 } 251 252 func getPrefixAndSlashFromDaemonPlatform() (prefix, slash string) { 253 if testEnv.DaemonInfo.OSType == "windows" { 254 return "c:", `\` 255 } 256 return "", "/" 257 } 258 259 func TestVolumePruneAnonymous(t *testing.T) { 260 ctx := setupTest(t) 261 262 client := testEnv.APIClient() 263 264 // Create an anonymous volume 265 v, err := client.VolumeCreate(ctx, volume.CreateOptions{}) 266 assert.NilError(t, err) 267 268 // Create a named volume 269 vNamed, err := client.VolumeCreate(ctx, volume.CreateOptions{ 270 Name: "test", 271 }) 272 assert.NilError(t, err) 273 274 // Prune anonymous volumes 275 pruneReport, err := client.VolumesPrune(ctx, filters.Args{}) 276 assert.NilError(t, err) 277 assert.Check(t, is.Equal(len(pruneReport.VolumesDeleted), 1)) 278 assert.Check(t, is.Equal(pruneReport.VolumesDeleted[0], v.Name)) 279 280 _, err = client.VolumeInspect(ctx, vNamed.Name) 281 assert.NilError(t, err) 282 283 // Prune all volumes 284 _, err = client.VolumeCreate(ctx, volume.CreateOptions{}) 285 assert.NilError(t, err) 286 287 pruneReport, err = client.VolumesPrune(ctx, filters.NewArgs(filters.Arg("all", "1"))) 288 assert.NilError(t, err) 289 assert.Check(t, is.Equal(len(pruneReport.VolumesDeleted), 2)) 290 291 // Validate that older API versions still have the old behavior of pruning all local volumes 292 clientOld, err := clientpkg.NewClientWithOpts(clientpkg.FromEnv, clientpkg.WithVersion("1.41")) 293 assert.NilError(t, err) 294 defer clientOld.Close() 295 assert.Equal(t, clientOld.ClientVersion(), "1.41") 296 297 v, err = client.VolumeCreate(ctx, volume.CreateOptions{}) 298 assert.NilError(t, err) 299 vNamed, err = client.VolumeCreate(ctx, volume.CreateOptions{Name: "test-api141"}) 300 assert.NilError(t, err) 301 302 pruneReport, err = clientOld.VolumesPrune(ctx, filters.Args{}) 303 assert.NilError(t, err) 304 assert.Check(t, is.Equal(len(pruneReport.VolumesDeleted), 2)) 305 assert.Check(t, is.Contains(pruneReport.VolumesDeleted, v.Name)) 306 assert.Check(t, is.Contains(pruneReport.VolumesDeleted, vNamed.Name)) 307 } 308 309 func TestVolumePruneAnonFromImage(t *testing.T) { 310 ctx := setupTest(t) 311 client := testEnv.APIClient() 312 313 volDest := "/foo" 314 if testEnv.DaemonInfo.OSType == "windows" { 315 volDest = `c:\\foo` 316 } 317 318 dockerfile := `FROM busybox 319 VOLUME ` + volDest 320 321 img := build.Do(ctx, t, client, fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile))) 322 323 id := container.Create(ctx, t, client, container.WithImage(img)) 324 defer client.ContainerRemove(ctx, id, containertypes.RemoveOptions{}) 325 326 inspect, err := client.ContainerInspect(ctx, id) 327 assert.NilError(t, err) 328 329 assert.Assert(t, is.Len(inspect.Mounts, 1)) 330 331 volumeName := inspect.Mounts[0].Name 332 assert.Assert(t, volumeName != "") 333 334 err = client.ContainerRemove(ctx, id, containertypes.RemoveOptions{}) 335 assert.NilError(t, err) 336 337 pruneReport, err := client.VolumesPrune(ctx, filters.Args{}) 338 assert.NilError(t, err) 339 assert.Assert(t, is.Contains(pruneReport.VolumesDeleted, volumeName)) 340 }