Files
moby/integration/volume/volume_test.go
Sebastiaan van Stijn dae3650dcc client: rename/deprecate WithVersion, WithVersionFromEnv
Add WithAPIVersion and WithAPIVersionFromEnv to be more clear on
the intent, and to align with other related options and fields.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-11-12 22:39:05 +01:00

361 lines
12 KiB
Go

package volume
import (
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"time"
cerrdefs "github.com/containerd/errdefs"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/api/types/volume"
"github.com/moby/moby/client"
"github.com/moby/moby/v2/integration/internal/build"
"github.com/moby/moby/v2/integration/internal/container"
"github.com/moby/moby/v2/internal/testutil"
"github.com/moby/moby/v2/internal/testutil/daemon"
"github.com/moby/moby/v2/internal/testutil/fakecontext"
"github.com/moby/moby/v2/internal/testutil/request"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/skip"
)
func TestVolumesCreateAndList(t *testing.T) {
ctx := setupTest(t)
apiClient := testEnv.APIClient()
name := t.Name()
// Windows file system is case insensitive
if testEnv.DaemonInfo.OSType == "windows" {
name = strings.ToLower(name)
}
created, err := apiClient.VolumeCreate(ctx, client.VolumeCreateOptions{
Name: name,
})
assert.NilError(t, err)
namedV := created.Volume
expected := volume.Volume{
// Ignore timestamp of CreatedAt
CreatedAt: namedV.CreatedAt,
Driver: "local",
Scope: "local",
Name: name,
Mountpoint: filepath.Join(testEnv.DaemonInfo.DockerRootDir, "volumes", name, "_data"),
}
assert.Check(t, is.DeepEqual(namedV, expected, cmpopts.EquateEmpty()))
res, err := apiClient.VolumeList(ctx, client.VolumeListOptions{})
assert.NilError(t, err)
assert.Assert(t, len(res.Items) > 0)
volumes := res.Items[:0]
for _, v := range res.Items {
if v.Name == namedV.Name {
volumes = append(volumes, v)
}
}
assert.Check(t, is.Equal(len(volumes), 1))
assert.Check(t, is.DeepEqual(volumes[0], expected, cmpopts.EquateEmpty()))
}
func TestVolumesRemove(t *testing.T) {
ctx := setupTest(t)
apiClient := testEnv.APIClient()
prefix, slash := getPrefixAndSlashFromDaemonPlatform()
id := container.Create(ctx, t, apiClient, container.WithVolume(prefix+slash+"foo"))
inspect, err := apiClient.ContainerInspect(ctx, id, client.ContainerInspectOptions{})
assert.NilError(t, err)
vname := inspect.Container.Mounts[0].Name
t.Run("volume in use", func(t *testing.T) {
_, err = apiClient.VolumeRemove(ctx, vname, client.VolumeRemoveOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsConflict))
assert.Check(t, is.ErrorContains(err, "volume is in use"))
})
t.Run("volume not in use", func(t *testing.T) {
_, err = apiClient.ContainerRemove(ctx, id, client.ContainerRemoveOptions{
Force: true,
})
assert.NilError(t, err)
_, err = apiClient.VolumeRemove(ctx, vname, client.VolumeRemoveOptions{})
assert.NilError(t, err)
})
t.Run("non-existing volume", func(t *testing.T) {
_, err = apiClient.VolumeRemove(ctx, "no_such_volume", client.VolumeRemoveOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
})
t.Run("non-existing volume force", func(t *testing.T) {
_, err = apiClient.VolumeRemove(ctx, "no_such_volume", client.VolumeRemoveOptions{Force: true})
assert.NilError(t, err)
})
}
// TestVolumesRemoveSwarmEnabled tests that an error is returned if a volume
// is in use, also if swarm is enabled (and cluster volumes are supported).
//
// Regression test for https://github.com/docker/cli/issues/4082
func TestVolumesRemoveSwarmEnabled(t *testing.T) {
skip.If(t, testEnv.IsRemoteDaemon, "cannot run daemon when remote daemon")
skip.If(t, testEnv.DaemonInfo.OSType == "windows", "TODO enable on windows")
ctx := setupTest(t)
t.Parallel()
// Spin up a new daemon, so that we can run this test in parallel (it's a slow test)
d := daemon.New(t)
d.StartAndSwarmInit(ctx, t)
defer d.Stop(t)
apiClient := d.NewClientT(t)
prefix, slash := getPrefixAndSlashFromDaemonPlatform()
id := container.Create(ctx, t, apiClient, container.WithVolume(prefix+slash+"foo"))
inspect, err := apiClient.ContainerInspect(ctx, id, client.ContainerInspectOptions{})
assert.NilError(t, err)
vname := inspect.Container.Mounts[0].Name
t.Run("volume in use", func(t *testing.T) {
_, err = apiClient.VolumeRemove(ctx, vname, client.VolumeRemoveOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsConflict))
assert.Check(t, is.ErrorContains(err, "volume is in use"))
})
t.Run("volume not in use", func(t *testing.T) {
_, err = apiClient.ContainerRemove(ctx, id, client.ContainerRemoveOptions{
Force: true,
})
assert.NilError(t, err)
_, err = apiClient.VolumeRemove(ctx, vname, client.VolumeRemoveOptions{})
assert.NilError(t, err)
})
t.Run("non-existing volume", func(t *testing.T) {
_, err = apiClient.VolumeRemove(ctx, "no_such_volume", client.VolumeRemoveOptions{})
assert.Check(t, is.ErrorType(err, cerrdefs.IsNotFound))
})
t.Run("non-existing volume force", func(t *testing.T) {
_, err = apiClient.VolumeRemove(ctx, "no_such_volume", client.VolumeRemoveOptions{Force: true})
assert.NilError(t, err)
})
}
func TestVolumesInspect(t *testing.T) {
ctx := setupTest(t)
apiClient := testEnv.APIClient()
now := time.Now()
created, err := apiClient.VolumeCreate(ctx, client.VolumeCreateOptions{})
assert.NilError(t, err)
v := created.Volume
res, err := apiClient.VolumeInspect(ctx, v.Name, client.VolumeInspectOptions{})
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(res.Volume, v, cmpopts.EquateEmpty()))
// comparing CreatedAt field time for the new volume to now. Truncate to 1 minute precision to avoid false positive
createdAt, err := time.Parse(time.RFC3339, strings.TrimSpace(res.Volume.CreatedAt))
assert.NilError(t, err)
assert.Check(t, createdAt.Unix()-now.Unix() < 60, "CreatedAt (%s) exceeds creation time (%s) 60s", createdAt, now)
// update atime and mtime for the "_data" directory (which would happen during volume initialization)
modifiedAt := time.Now().Local().Add(5 * time.Hour)
err = os.Chtimes(res.Volume.Mountpoint, modifiedAt, modifiedAt)
assert.NilError(t, err)
res, err = apiClient.VolumeInspect(ctx, v.Name, client.VolumeInspectOptions{})
assert.NilError(t, err)
createdAt2, err := time.Parse(time.RFC3339, strings.TrimSpace(res.Volume.CreatedAt))
assert.NilError(t, err)
// Check that CreatedAt didn't change after updating atime and mtime of the "_data" directory
// Related issue: #38274
assert.Equal(t, createdAt, createdAt2)
}
// TestVolumesInvalidJSON tests that POST endpoints that expect a body return
// the correct error when sending invalid JSON requests.
func TestVolumesInvalidJSON(t *testing.T) {
ctx := setupTest(t)
// POST endpoints that accept / expect a JSON body;
endpoints := []string{"/volumes/create"}
for _, ep := range endpoints {
t.Run(ep[1:], func(t *testing.T) {
t.Parallel()
ctx := testutil.StartSpan(ctx, t)
t.Run("invalid content type", func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
res, body, err := request.Post(ctx, ep, request.RawString("{}"), request.ContentType("text/plain"))
assert.NilError(t, err)
assert.Check(t, is.Equal(res.StatusCode, http.StatusBadRequest))
buf, err := request.ReadBody(body)
assert.NilError(t, err)
assert.Check(t, is.Contains(string(buf), "unsupported Content-Type header (text/plain): must be 'application/json'"))
})
t.Run("invalid JSON", func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
res, body, err := request.Post(ctx, ep, request.RawString("{invalid json"), request.JSON)
assert.NilError(t, err)
assert.Check(t, is.Equal(res.StatusCode, http.StatusBadRequest))
buf, err := request.ReadBody(body)
assert.NilError(t, err)
assert.Check(t, is.Contains(string(buf), "invalid JSON: invalid character 'i' looking for beginning of object key string"))
})
t.Run("extra content after JSON", func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
res, body, err := request.Post(ctx, ep, request.RawString(`{} trailing content`), request.JSON)
assert.NilError(t, err)
assert.Check(t, is.Equal(res.StatusCode, http.StatusBadRequest))
buf, err := request.ReadBody(body)
assert.NilError(t, err)
assert.Check(t, is.Contains(string(buf), "unexpected content after JSON"))
})
t.Run("empty body", func(t *testing.T) {
ctx := testutil.StartSpan(ctx, t)
// empty body should not produce an 500 internal server error, or
// any 5XX error (this is assuming the request does not produce
// an internal server error for another reason, but it shouldn't)
res, _, err := request.Post(ctx, ep, request.RawString(``), request.JSON)
assert.NilError(t, err)
assert.Check(t, res.StatusCode < http.StatusInternalServerError)
})
})
}
}
func getPrefixAndSlashFromDaemonPlatform() (prefix, slash string) {
if testEnv.DaemonInfo.OSType == "windows" {
return "c:", `\`
}
return "", "/"
}
func TestVolumePruneAnonymous(t *testing.T) {
ctx := setupTest(t)
apiClient := testEnv.APIClient()
// Create an anonymous volume
created, err := apiClient.VolumeCreate(ctx, client.VolumeCreateOptions{})
assert.NilError(t, err)
anonV := created.Volume
// Create a named volume
created, err = apiClient.VolumeCreate(ctx, client.VolumeCreateOptions{
Name: "test",
})
assert.NilError(t, err)
namedV := created.Volume
// Prune anonymous volumes
prune, err := apiClient.VolumePrune(ctx, client.VolumePruneOptions{})
assert.NilError(t, err)
report := prune.Report
assert.Check(t, is.Equal(len(report.VolumesDeleted), 1))
assert.Check(t, is.Equal(report.VolumesDeleted[0], anonV.Name))
_, err = apiClient.VolumeInspect(ctx, namedV.Name, client.VolumeInspectOptions{})
assert.NilError(t, err)
// Prune all volumes
_, err = apiClient.VolumeCreate(ctx, client.VolumeCreateOptions{})
assert.NilError(t, err)
prune, err = apiClient.VolumePrune(ctx, client.VolumePruneOptions{
All: true,
})
assert.NilError(t, err)
assert.Check(t, is.Equal(len(prune.Report.VolumesDeleted), 2))
// Create a named volume and an anonymous volume, and prune all
_, err = apiClient.VolumeCreate(ctx, client.VolumeCreateOptions{})
assert.NilError(t, err)
_, err = apiClient.VolumeCreate(ctx, client.VolumeCreateOptions{Name: "test"})
assert.NilError(t, err)
prune, err = apiClient.VolumePrune(ctx, client.VolumePruneOptions{
All: true,
})
assert.NilError(t, err)
report = prune.Report
assert.Check(t, is.Equal(len(report.VolumesDeleted), 2))
// Validate that older API versions still have the old behavior of pruning all local volumes
clientOld, err := client.New(client.FromEnv, client.WithAPIVersion("1.41"))
assert.NilError(t, err)
defer clientOld.Close()
assert.Equal(t, clientOld.ClientVersion(), "1.41")
created, err = apiClient.VolumeCreate(ctx, client.VolumeCreateOptions{})
assert.NilError(t, err)
anonV = created.Volume
created, err = apiClient.VolumeCreate(ctx, client.VolumeCreateOptions{Name: "test-api141"})
assert.NilError(t, err)
namedV = created.Volume
prune, err = clientOld.VolumePrune(ctx, client.VolumePruneOptions{})
assert.NilError(t, err)
report = prune.Report
assert.Check(t, is.Equal(len(report.VolumesDeleted), 2))
assert.Check(t, is.Contains(report.VolumesDeleted, anonV.Name))
assert.Check(t, is.Contains(report.VolumesDeleted, namedV.Name))
}
func TestVolumePruneAnonFromImage(t *testing.T) {
ctx := setupTest(t)
apiClient := testEnv.APIClient()
volDest := "/foo"
if testEnv.DaemonInfo.OSType == "windows" {
volDest = `c:\\foo`
}
dockerfile := `FROM busybox
VOLUME ` + volDest
img := build.Do(ctx, t, apiClient, fakecontext.New(t, "", fakecontext.WithDockerfile(dockerfile)))
id := container.Create(ctx, t, apiClient, container.WithImage(img))
defer apiClient.ContainerRemove(ctx, id, client.ContainerRemoveOptions{})
inspect, err := apiClient.ContainerInspect(ctx, id, client.ContainerInspectOptions{})
assert.NilError(t, err)
assert.Assert(t, is.Len(inspect.Container.Mounts, 1))
volumeName := inspect.Container.Mounts[0].Name
assert.Assert(t, volumeName != "")
_, err = apiClient.ContainerRemove(ctx, id, client.ContainerRemoveOptions{})
assert.NilError(t, err)
res, err := apiClient.VolumePrune(ctx, client.VolumePruneOptions{})
assert.NilError(t, err)
assert.Assert(t, is.Contains(res.Report.VolumesDeleted, volumeName))
}