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