github.com/moby/docker@v26.1.3+incompatible/volume/local/local_linux_test.go (about)

     1  //go:build linux
     2  
     3  package local // import "github.com/docker/docker/volume/local"
     4  
     5  import (
     6  	"net"
     7  	"os"
     8  	"path/filepath"
     9  	"strconv"
    10  	"testing"
    11  
    12  	"github.com/docker/docker/errdefs"
    13  	"github.com/docker/docker/pkg/idtools"
    14  	"github.com/docker/docker/quota"
    15  	"gotest.tools/v3/assert"
    16  	is "gotest.tools/v3/assert/cmp"
    17  )
    18  
    19  const (
    20  	quotaSize        = 1024 * 1024
    21  	quotaSizeLiteral = "1M"
    22  )
    23  
    24  func TestQuota(t *testing.T) {
    25  	if msg, ok := quota.CanTestQuota(); !ok {
    26  		t.Skip(msg)
    27  	}
    28  
    29  	// get sparse xfs test image
    30  	imageFileName, err := quota.PrepareQuotaTestImage(t)
    31  	if err != nil {
    32  		t.Fatal(err)
    33  	}
    34  	defer os.Remove(imageFileName)
    35  
    36  	t.Run("testVolWithQuota", quota.WrapMountTest(imageFileName, true, testVolWithQuota))
    37  	t.Run("testVolQuotaUnsupported", quota.WrapMountTest(imageFileName, false, testVolQuotaUnsupported))
    38  }
    39  
    40  func testVolWithQuota(t *testing.T, mountPoint, backingFsDev, testDir string) {
    41  	r, err := New(testDir, idtools.Identity{UID: os.Geteuid(), GID: os.Getegid()})
    42  	if err != nil {
    43  		t.Fatal(err)
    44  	}
    45  	assert.Assert(t, r.quotaCtl != nil)
    46  
    47  	vol, err := r.Create("testing", map[string]string{"size": quotaSizeLiteral})
    48  	if err != nil {
    49  		t.Fatal(err)
    50  	}
    51  
    52  	dir, err := vol.Mount("1234")
    53  	if err != nil {
    54  		t.Fatal(err)
    55  	}
    56  	defer func() {
    57  		if err := vol.Unmount("1234"); err != nil {
    58  			t.Fatal(err)
    59  		}
    60  	}()
    61  
    62  	testfile := filepath.Join(dir, "testfile")
    63  
    64  	// test writing file smaller than quota
    65  	assert.NilError(t, os.WriteFile(testfile, make([]byte, quotaSize/2), 0o644))
    66  	assert.NilError(t, os.Remove(testfile))
    67  
    68  	// test writing fiel larger than quota
    69  	err = os.WriteFile(testfile, make([]byte, quotaSize+1), 0o644)
    70  	assert.ErrorContains(t, err, "")
    71  	if _, err := os.Stat(testfile); err == nil {
    72  		assert.NilError(t, os.Remove(testfile))
    73  	}
    74  }
    75  
    76  func testVolQuotaUnsupported(t *testing.T, mountPoint, backingFsDev, testDir string) {
    77  	r, err := New(testDir, idtools.Identity{UID: os.Geteuid(), GID: os.Getegid()})
    78  	if err != nil {
    79  		t.Fatal(err)
    80  	}
    81  	assert.Assert(t, is.Nil(r.quotaCtl))
    82  
    83  	_, err = r.Create("testing", map[string]string{"size": quotaSizeLiteral})
    84  	assert.ErrorContains(t, err, "no quota support")
    85  
    86  	vol, err := r.Create("testing", nil)
    87  	if err != nil {
    88  		t.Fatal(err)
    89  	}
    90  
    91  	// this could happen if someone moves volumes from storage with
    92  	// quota support to some place without
    93  	lv, ok := vol.(*localVolume)
    94  	assert.Assert(t, ok)
    95  	lv.opts = &optsConfig{
    96  		Quota: quota.Quota{Size: quotaSize},
    97  	}
    98  
    99  	_, err = vol.Mount("1234")
   100  	assert.ErrorContains(t, err, "no quota support")
   101  }
   102  
   103  func TestVolCreateValidation(t *testing.T) {
   104  	r, err := New(t.TempDir(), idtools.Identity{UID: os.Geteuid(), GID: os.Getegid()})
   105  	if err != nil {
   106  		t.Fatal(err)
   107  	}
   108  
   109  	mandatoryOpts = map[string][]string{
   110  		"device": {"type"},
   111  		"type":   {"device"},
   112  		"o":      {"device", "type"},
   113  	}
   114  
   115  	tests := []struct {
   116  		doc         string
   117  		name        string
   118  		opts        map[string]string
   119  		expectedErr string
   120  	}{
   121  		{
   122  			doc:  "invalid: name too short",
   123  			name: "a",
   124  			opts: map[string]string{
   125  				"type":   "foo",
   126  				"device": "foo",
   127  			},
   128  			expectedErr: `volume name is too short, names should be at least two alphanumeric characters`,
   129  		},
   130  		{
   131  			doc:  "invalid: name invalid characters",
   132  			name: "hello world",
   133  			opts: map[string]string{
   134  				"type":   "foo",
   135  				"device": "foo",
   136  			},
   137  			expectedErr: `"hello world" includes invalid characters for a local volume name, only "[a-zA-Z0-9][a-zA-Z0-9_.-]" are allowed. If you intended to pass a host directory, use absolute path`,
   138  		},
   139  		{
   140  			doc:         "invalid: unknown option",
   141  			opts:        map[string]string{"hello": "world"},
   142  			expectedErr: `invalid option: "hello"`,
   143  		},
   144  		{
   145  			doc:         "invalid: invalid size",
   146  			opts:        map[string]string{"size": "hello"},
   147  			expectedErr: `invalid size: 'hello'`,
   148  		},
   149  		{
   150  			doc:         "invalid: size, but no quotactl",
   151  			opts:        map[string]string{"size": "1234"},
   152  			expectedErr: `quota size requested but no quota support`,
   153  		},
   154  		{
   155  			doc: "invalid: device without type",
   156  			opts: map[string]string{
   157  				"device": "foo",
   158  			},
   159  			expectedErr: `missing required option: "type"`,
   160  		},
   161  		{
   162  			doc: "invalid: type without device",
   163  			opts: map[string]string{
   164  				"type": "foo",
   165  			},
   166  			expectedErr: `missing required option: "device"`,
   167  		},
   168  		{
   169  			doc: "invalid: o without device",
   170  			opts: map[string]string{
   171  				"o":    "foo",
   172  				"type": "foo",
   173  			},
   174  			expectedErr: `missing required option: "device"`,
   175  		},
   176  		{
   177  			doc: "invalid: o without type",
   178  			opts: map[string]string{
   179  				"o":      "foo",
   180  				"device": "foo",
   181  			},
   182  			expectedErr: `missing required option: "type"`,
   183  		},
   184  		{
   185  			doc:  "valid: short name, no options",
   186  			name: "ab",
   187  		},
   188  		{
   189  			doc: "valid: device and type",
   190  			opts: map[string]string{
   191  				"type":   "foo",
   192  				"device": "foo",
   193  			},
   194  		},
   195  		{
   196  			doc: "valid: device, type, and o",
   197  			opts: map[string]string{
   198  				"type":   "foo",
   199  				"device": "foo",
   200  				"o":      "foo",
   201  			},
   202  		},
   203  		{
   204  			doc: "cifs",
   205  			opts: map[string]string{
   206  				"type":   "cifs",
   207  				"device": "//some.example.com/thepath",
   208  				"o":      "foo",
   209  			},
   210  		},
   211  		{
   212  			doc: "cifs with port in url",
   213  			opts: map[string]string{
   214  				"type":   "cifs",
   215  				"device": "//some.example.com:2345/thepath",
   216  				"o":      "foo",
   217  			},
   218  			expectedErr: "port not allowed in CIFS device URL, include 'port' in 'o='",
   219  		},
   220  		{
   221  			doc: "cifs with bad url",
   222  			opts: map[string]string{
   223  				"type":   "cifs",
   224  				"device": ":::",
   225  				"o":      "foo",
   226  			},
   227  			expectedErr: `error parsing mount device url: parse ":::": missing protocol scheme`,
   228  		},
   229  	}
   230  
   231  	for i, tc := range tests {
   232  		tc := tc
   233  		t.Run(tc.doc, func(t *testing.T) {
   234  			if tc.name == "" {
   235  				tc.name = "vol-" + strconv.Itoa(i)
   236  			}
   237  			v, err := r.Create(tc.name, tc.opts)
   238  			if v != nil {
   239  				defer assert.Check(t, r.Remove(v))
   240  			}
   241  			if tc.expectedErr == "" {
   242  				assert.NilError(t, err)
   243  			} else {
   244  				assert.Check(t, errdefs.IsInvalidParameter(err), "got: %T", err)
   245  				assert.ErrorContains(t, err, tc.expectedErr)
   246  			}
   247  		})
   248  	}
   249  }
   250  
   251  func TestVolMountOpts(t *testing.T) {
   252  	tests := []struct {
   253  		name                         string
   254  		opts                         optsConfig
   255  		expectedErr                  string
   256  		expectedDevice, expectedOpts string
   257  	}{
   258  		{
   259  			name: "cifs url with space",
   260  			opts: optsConfig{
   261  				MountType:   "cifs",
   262  				MountDevice: "//1.2.3.4/Program Files",
   263  			},
   264  			expectedDevice: "//1.2.3.4/Program Files",
   265  			expectedOpts:   "",
   266  		},
   267  		{
   268  			name: "cifs resolve addr",
   269  			opts: optsConfig{
   270  				MountType:   "cifs",
   271  				MountDevice: "//example.com/Program Files",
   272  				MountOpts:   "addr=example.com",
   273  			},
   274  			expectedDevice: "//example.com/Program Files",
   275  			expectedOpts:   "addr=1.2.3.4",
   276  		},
   277  		{
   278  			name: "cifs resolve device",
   279  			opts: optsConfig{
   280  				MountType:   "cifs",
   281  				MountDevice: "//example.com/Program Files",
   282  			},
   283  			expectedDevice: "//1.2.3.4/Program Files",
   284  		},
   285  		{
   286  			name: "nfs dont resolve device",
   287  			opts: optsConfig{
   288  				MountType:   "nfs",
   289  				MountDevice: "//example.com/Program Files",
   290  			},
   291  			expectedDevice: "//example.com/Program Files",
   292  		},
   293  		{
   294  			name: "nfs resolve addr",
   295  			opts: optsConfig{
   296  				MountType:   "nfs",
   297  				MountDevice: "//example.com/Program Files",
   298  				MountOpts:   "addr=example.com",
   299  			},
   300  			expectedDevice: "//example.com/Program Files",
   301  			expectedOpts:   "addr=1.2.3.4",
   302  		},
   303  	}
   304  
   305  	ip1234 := net.ParseIP("1.2.3.4")
   306  	resolveIP := func(network, addr string) (*net.IPAddr, error) {
   307  		switch addr {
   308  		case "example.com":
   309  			return &net.IPAddr{IP: ip1234}, nil
   310  		}
   311  
   312  		return nil, &net.DNSError{Err: "no such host", Name: addr, IsNotFound: true}
   313  	}
   314  
   315  	for _, tc := range tests {
   316  		tc := tc
   317  		t.Run(tc.name, func(t *testing.T) {
   318  			dev, opts, err := getMountOptions(&tc.opts, resolveIP)
   319  
   320  			if tc.expectedErr != "" {
   321  				assert.Check(t, is.ErrorContains(err, tc.expectedErr))
   322  			} else {
   323  				assert.Check(t, err)
   324  			}
   325  
   326  			assert.Check(t, is.Equal(dev, tc.expectedDevice))
   327  			assert.Check(t, is.Equal(opts, tc.expectedOpts))
   328  		})
   329  	}
   330  }