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  }