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  }