//go:build linux package local // import "github.com/docker/docker/volume/local" import ( "net" "os" "path/filepath" "strconv" "testing" "github.com/docker/docker/errdefs" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/quota" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) const ( quotaSize = 1024 * 1024 quotaSizeLiteral = "1M" ) func TestQuota(t *testing.T) { if msg, ok := quota.CanTestQuota(); !ok { t.Skip(msg) } // get sparse xfs test image imageFileName, err := quota.PrepareQuotaTestImage(t) if err != nil { t.Fatal(err) } defer os.Remove(imageFileName) t.Run("testVolWithQuota", quota.WrapMountTest(imageFileName, true, testVolWithQuota)) t.Run("testVolQuotaUnsupported", quota.WrapMountTest(imageFileName, false, testVolQuotaUnsupported)) } func testVolWithQuota(t *testing.T, mountPoint, backingFsDev, testDir string) { r, err := New(testDir, idtools.Identity{UID: os.Geteuid(), GID: os.Getegid()}) if err != nil { t.Fatal(err) } assert.Assert(t, r.quotaCtl != nil) vol, err := r.Create("testing", map[string]string{"size": quotaSizeLiteral}) if err != nil { t.Fatal(err) } dir, err := vol.Mount("1234") if err != nil { t.Fatal(err) } defer func() { if err := vol.Unmount("1234"); err != nil { t.Fatal(err) } }() testfile := filepath.Join(dir, "testfile") // test writing file smaller than quota assert.NilError(t, os.WriteFile(testfile, make([]byte, quotaSize/2), 0o644)) assert.NilError(t, os.Remove(testfile)) // test writing fiel larger than quota err = os.WriteFile(testfile, make([]byte, quotaSize+1), 0o644) assert.ErrorContains(t, err, "") if _, err := os.Stat(testfile); err == nil { assert.NilError(t, os.Remove(testfile)) } } func testVolQuotaUnsupported(t *testing.T, mountPoint, backingFsDev, testDir string) { r, err := New(testDir, idtools.Identity{UID: os.Geteuid(), GID: os.Getegid()}) if err != nil { t.Fatal(err) } assert.Assert(t, is.Nil(r.quotaCtl)) _, err = r.Create("testing", map[string]string{"size": quotaSizeLiteral}) assert.ErrorContains(t, err, "no quota support") vol, err := r.Create("testing", nil) if err != nil { t.Fatal(err) } // this could happen if someone moves volumes from storage with // quota support to some place without lv, ok := vol.(*localVolume) assert.Assert(t, ok) lv.opts = &optsConfig{ Quota: quota.Quota{Size: quotaSize}, } _, err = vol.Mount("1234") assert.ErrorContains(t, err, "no quota support") } func TestVolCreateValidation(t *testing.T) { r, err := New(t.TempDir(), idtools.Identity{UID: os.Geteuid(), GID: os.Getegid()}) if err != nil { t.Fatal(err) } mandatoryOpts = map[string][]string{ "device": {"type"}, "type": {"device"}, "o": {"device", "type"}, } tests := []struct { doc string name string opts map[string]string expectedErr string }{ { doc: "invalid: name too short", name: "a", opts: map[string]string{ "type": "foo", "device": "foo", }, expectedErr: `volume name is too short, names should be at least two alphanumeric characters`, }, { doc: "invalid: name invalid characters", name: "hello world", opts: map[string]string{ "type": "foo", "device": "foo", }, 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`, }, { doc: "invalid: unknown option", opts: map[string]string{"hello": "world"}, expectedErr: `invalid option: "hello"`, }, { doc: "invalid: invalid size", opts: map[string]string{"size": "hello"}, expectedErr: `invalid size: 'hello'`, }, { doc: "invalid: size, but no quotactl", opts: map[string]string{"size": "1234"}, expectedErr: `quota size requested but no quota support`, }, { doc: "invalid: device without type", opts: map[string]string{ "device": "foo", }, expectedErr: `missing required option: "type"`, }, { doc: "invalid: type without device", opts: map[string]string{ "type": "foo", }, expectedErr: `missing required option: "device"`, }, { doc: "invalid: o without device", opts: map[string]string{ "o": "foo", "type": "foo", }, expectedErr: `missing required option: "device"`, }, { doc: "invalid: o without type", opts: map[string]string{ "o": "foo", "device": "foo", }, expectedErr: `missing required option: "type"`, }, { doc: "valid: short name, no options", name: "ab", }, { doc: "valid: device and type", opts: map[string]string{ "type": "foo", "device": "foo", }, }, { doc: "valid: device, type, and o", opts: map[string]string{ "type": "foo", "device": "foo", "o": "foo", }, }, { doc: "cifs", opts: map[string]string{ "type": "cifs", "device": "//some.example.com/thepath", "o": "foo", }, }, { doc: "cifs with port in url", opts: map[string]string{ "type": "cifs", "device": "//some.example.com:2345/thepath", "o": "foo", }, expectedErr: "port not allowed in CIFS device URL, include 'port' in 'o='", }, { doc: "cifs with bad url", opts: map[string]string{ "type": "cifs", "device": ":::", "o": "foo", }, expectedErr: `error parsing mount device url: parse ":::": missing protocol scheme`, }, } for i, tc := range tests { tc := tc t.Run(tc.doc, func(t *testing.T) { if tc.name == "" { tc.name = "vol-" + strconv.Itoa(i) } v, err := r.Create(tc.name, tc.opts) if v != nil { defer assert.Check(t, r.Remove(v)) } if tc.expectedErr == "" { assert.NilError(t, err) } else { assert.Check(t, errdefs.IsInvalidParameter(err), "got: %T", err) assert.ErrorContains(t, err, tc.expectedErr) } }) } } func TestVolMountOpts(t *testing.T) { tests := []struct { name string opts optsConfig expectedErr string expectedDevice, expectedOpts string }{ { name: "cifs url with space", opts: optsConfig{ MountType: "cifs", MountDevice: "//1.2.3.4/Program Files", }, expectedDevice: "//1.2.3.4/Program Files", expectedOpts: "", }, { name: "cifs resolve addr", opts: optsConfig{ MountType: "cifs", MountDevice: "//example.com/Program Files", MountOpts: "addr=example.com", }, expectedDevice: "//example.com/Program Files", expectedOpts: "addr=1.2.3.4", }, { name: "cifs resolve device", opts: optsConfig{ MountType: "cifs", MountDevice: "//example.com/Program Files", }, expectedDevice: "//1.2.3.4/Program Files", }, { name: "nfs dont resolve device", opts: optsConfig{ MountType: "nfs", MountDevice: "//example.com/Program Files", }, expectedDevice: "//example.com/Program Files", }, { name: "nfs resolve addr", opts: optsConfig{ MountType: "nfs", MountDevice: "//example.com/Program Files", MountOpts: "addr=example.com", }, expectedDevice: "//example.com/Program Files", expectedOpts: "addr=1.2.3.4", }, } ip1234 := net.ParseIP("1.2.3.4") resolveIP := func(network, addr string) (*net.IPAddr, error) { switch addr { case "example.com": return &net.IPAddr{IP: ip1234}, nil } return nil, &net.DNSError{Err: "no such host", Name: addr, IsNotFound: true} } for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { dev, opts, err := getMountOptions(&tc.opts, resolveIP) if tc.expectedErr != "" { assert.Check(t, is.ErrorContains(err, tc.expectedErr)) } else { assert.Check(t, err) } assert.Check(t, is.Equal(dev, tc.expectedDevice)) assert.Check(t, is.Equal(opts, tc.expectedOpts)) }) } }