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  }