mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
bb934c6aca
Functions `GetMounts()` and `parseMountTable()` return all the entries as read and parsed from /proc/self/mountinfo. In many cases the caller is only interested only one or a few entries, not all of them. One good example is `Mounted()` function, which looks for a specific entry only. Another example is `RecursiveUnmount()` which is only interested in mount under a specific path. This commit adds `filter` argument to `GetMounts()` to implement two things: 1. filter out entries a caller is not interested in 2. stop processing if a caller is found what it wanted `nil` can be passed to get a backward-compatible behavior, i.e. return all the entries. A few filters are implemented: - `PrefixFilter`: filters out all entries not under `prefix` - `SingleEntryFilter`: looks for a specific entry Finally, `Mounted()` is modified to use `SingleEntryFilter()`, and `RecursiveUnmount()` is using `PrefixFilter()`. Unit tests are added to check filters are working. [v2: ditch NoFilter, use nil] [v3: ditch GetMountsFiltered()] [v4: add unit test for filters] [v5: switch to gotestyourself] Signed-off-by: Kir Kolyshkin <kolyshkin@gmail.com>
234 lines
5.8 KiB
Go
234 lines
5.8 KiB
Go
// +build linux
|
|
|
|
package mount // import "github.com/docker/docker/pkg/mount"
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
selinux "github.com/opencontainers/selinux/go-selinux"
|
|
)
|
|
|
|
func TestMount(t *testing.T) {
|
|
if os.Getuid() != 0 {
|
|
t.Skip("root required")
|
|
}
|
|
|
|
source, err := ioutil.TempDir("", "mount-test-source-")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(source)
|
|
|
|
// Ensure we have a known start point by mounting tmpfs with given options
|
|
if err := Mount("tmpfs", source, "tmpfs", "private"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ensureUnmount(t, source)
|
|
validateMount(t, source, "", "", "")
|
|
if t.Failed() {
|
|
t.FailNow()
|
|
}
|
|
|
|
target, err := ioutil.TempDir("", "mount-test-target-")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(target)
|
|
|
|
tests := []struct {
|
|
source string
|
|
ftype string
|
|
options string
|
|
expectedOpts string
|
|
expectedOptional string
|
|
expectedVFS string
|
|
}{
|
|
// No options
|
|
{"tmpfs", "tmpfs", "", "", "", ""},
|
|
// Default rw / ro test
|
|
{source, "", "bind", "", "", ""},
|
|
{source, "", "bind,private", "", "", ""},
|
|
{source, "", "bind,shared", "", "shared", ""},
|
|
{source, "", "bind,slave", "", "master", ""},
|
|
{source, "", "bind,unbindable", "", "unbindable", ""},
|
|
// Read Write tests
|
|
{source, "", "bind,rw", "rw", "", ""},
|
|
{source, "", "bind,rw,private", "rw", "", ""},
|
|
{source, "", "bind,rw,shared", "rw", "shared", ""},
|
|
{source, "", "bind,rw,slave", "rw", "master", ""},
|
|
{source, "", "bind,rw,unbindable", "rw", "unbindable", ""},
|
|
// Read Only tests
|
|
{source, "", "bind,ro", "ro", "", ""},
|
|
{source, "", "bind,ro,private", "ro", "", ""},
|
|
{source, "", "bind,ro,shared", "ro", "shared", ""},
|
|
{source, "", "bind,ro,slave", "ro", "master", ""},
|
|
{source, "", "bind,ro,unbindable", "ro", "unbindable", ""},
|
|
// Remount tests to change per filesystem options
|
|
{"", "", "remount,size=128k", "rw", "", "rw,size=128k"},
|
|
{"", "", "remount,ro,size=128k", "ro", "", "ro,size=128k"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
ftype, options := tc.ftype, tc.options
|
|
if tc.ftype == "" {
|
|
ftype = "none"
|
|
}
|
|
if tc.options == "" {
|
|
options = "none"
|
|
}
|
|
|
|
t.Run(fmt.Sprintf("%v-%v", ftype, options), func(t *testing.T) {
|
|
if strings.Contains(tc.options, "slave") {
|
|
// Slave requires a shared source
|
|
if err := MakeShared(source); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
if err := MakePrivate(source); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}()
|
|
}
|
|
if strings.Contains(tc.options, "remount") {
|
|
// create a new mount to remount first
|
|
if err := Mount("tmpfs", target, "tmpfs", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := Mount(tc.source, target, tc.ftype, tc.options); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ensureUnmount(t, target)
|
|
expectedVFS := tc.expectedVFS
|
|
if selinux.GetEnabled() && expectedVFS != "" {
|
|
expectedVFS = expectedVFS + ",seclabel"
|
|
}
|
|
validateMount(t, target, tc.expectedOpts, tc.expectedOptional, expectedVFS)
|
|
})
|
|
}
|
|
}
|
|
|
|
// ensureUnmount umounts mnt checking for errors
|
|
func ensureUnmount(t *testing.T, mnt string) {
|
|
if err := Unmount(mnt); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
// validateMount checks that mnt has the given options
|
|
func validateMount(t *testing.T, mnt string, opts, optional, vfs string) {
|
|
info, err := GetMounts(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
wantedOpts := make(map[string]struct{})
|
|
if opts != "" {
|
|
for _, opt := range strings.Split(opts, ",") {
|
|
wantedOpts[opt] = struct{}{}
|
|
}
|
|
}
|
|
|
|
wantedOptional := make(map[string]struct{})
|
|
if optional != "" {
|
|
for _, opt := range strings.Split(optional, ",") {
|
|
wantedOptional[opt] = struct{}{}
|
|
}
|
|
}
|
|
|
|
wantedVFS := make(map[string]struct{})
|
|
if vfs != "" {
|
|
for _, opt := range strings.Split(vfs, ",") {
|
|
wantedVFS[opt] = struct{}{}
|
|
}
|
|
}
|
|
|
|
mnts := make(map[int]*Info, len(info))
|
|
for _, mi := range info {
|
|
mnts[mi.ID] = mi
|
|
}
|
|
|
|
for _, mi := range info {
|
|
if mi.Mountpoint != mnt {
|
|
continue
|
|
}
|
|
|
|
// Use parent info as the defaults
|
|
p := mnts[mi.Parent]
|
|
pOpts := make(map[string]struct{})
|
|
if p.Opts != "" {
|
|
for _, opt := range strings.Split(p.Opts, ",") {
|
|
pOpts[clean(opt)] = struct{}{}
|
|
}
|
|
}
|
|
pOptional := make(map[string]struct{})
|
|
if p.Optional != "" {
|
|
for _, field := range strings.Split(p.Optional, ",") {
|
|
pOptional[clean(field)] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// Validate Opts
|
|
if mi.Opts != "" {
|
|
for _, opt := range strings.Split(mi.Opts, ",") {
|
|
opt = clean(opt)
|
|
if !has(wantedOpts, opt) && !has(pOpts, opt) {
|
|
t.Errorf("unexpected mount option %q expected %q", opt, opts)
|
|
}
|
|
delete(wantedOpts, opt)
|
|
}
|
|
}
|
|
for opt := range wantedOpts {
|
|
t.Errorf("missing mount option %q found %q", opt, mi.Opts)
|
|
}
|
|
|
|
// Validate Optional
|
|
if mi.Optional != "" {
|
|
for _, field := range strings.Split(mi.Optional, ",") {
|
|
field = clean(field)
|
|
if !has(wantedOptional, field) && !has(pOptional, field) {
|
|
t.Errorf("unexpected optional failed %q expected %q", field, optional)
|
|
}
|
|
delete(wantedOptional, field)
|
|
}
|
|
}
|
|
for field := range wantedOptional {
|
|
t.Errorf("missing optional field %q found %q", field, mi.Optional)
|
|
}
|
|
|
|
// Validate VFS if set
|
|
if vfs != "" {
|
|
if mi.VfsOpts != "" {
|
|
for _, opt := range strings.Split(mi.VfsOpts, ",") {
|
|
opt = clean(opt)
|
|
if !has(wantedVFS, opt) {
|
|
t.Errorf("unexpected mount option %q expected %q", opt, vfs)
|
|
}
|
|
delete(wantedVFS, opt)
|
|
}
|
|
}
|
|
for opt := range wantedVFS {
|
|
t.Errorf("missing mount option %q found %q", opt, mi.VfsOpts)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
t.Errorf("failed to find mount %q", mnt)
|
|
}
|
|
|
|
// clean strips off any value param after the colon
|
|
func clean(v string) string {
|
|
return strings.SplitN(v, ":", 2)[0]
|
|
}
|
|
|
|
// has returns true if key is a member of m
|
|
func has(m map[string]struct{}, key string) bool {
|
|
_, ok := m[key]
|
|
return ok
|
|
}
|