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

     1  package mounts // import "github.com/docker/docker/volume/mounts"
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  	"testing"
     7  
     8  	"github.com/docker/docker/api/types/mount"
     9  	"gotest.tools/v3/assert"
    10  	is "gotest.tools/v3/assert/cmp"
    11  )
    12  
    13  func TestLinuxParseMountRaw(t *testing.T) {
    14  	valid := []string{
    15  		"/home",
    16  		"/home:/home",
    17  		"/home:/something/else",
    18  		"/with space",
    19  		"/home:/with space",
    20  		"relative:/absolute-path",
    21  		"hostPath:/containerPath:ro",
    22  		"/hostPath:/containerPath:rw",
    23  		"/rw:/ro",
    24  		"/hostPath:/containerPath:shared",
    25  		"/hostPath:/containerPath:rshared",
    26  		"/hostPath:/containerPath:slave",
    27  		"/hostPath:/containerPath:rslave",
    28  		"/hostPath:/containerPath:private",
    29  		"/hostPath:/containerPath:rprivate",
    30  		"/hostPath:/containerPath:ro,shared",
    31  		"/hostPath:/containerPath:ro,slave",
    32  		"/hostPath:/containerPath:ro,private",
    33  		"/hostPath:/containerPath:ro,z,shared",
    34  		"/hostPath:/containerPath:ro,Z,slave",
    35  		"/hostPath:/containerPath:Z,ro,slave",
    36  		"/hostPath:/containerPath:slave,Z,ro",
    37  		"/hostPath:/containerPath:Z,slave,ro",
    38  		"/hostPath:/containerPath:slave,ro,Z",
    39  		"/hostPath:/containerPath:rslave,ro,Z",
    40  		"/hostPath:/containerPath:ro,rshared,Z",
    41  		"/hostPath:/containerPath:ro,Z,rprivate",
    42  	}
    43  
    44  	invalid := map[string]string{
    45  		"":                                "invalid volume specification",
    46  		"./":                              "mount path must be absolute",
    47  		"../":                             "mount path must be absolute",
    48  		"/:../":                           "mount path must be absolute",
    49  		"/:path":                          "mount path must be absolute",
    50  		":":                               "invalid volume specification",
    51  		"/tmp:":                           "invalid volume specification",
    52  		":test":                           "invalid volume specification",
    53  		":/test":                          "invalid volume specification",
    54  		"tmp:":                            "invalid volume specification",
    55  		":test:":                          "invalid volume specification",
    56  		"::":                              "invalid volume specification",
    57  		":::":                             "invalid volume specification",
    58  		"/tmp:::":                         "invalid volume specification",
    59  		":/tmp::":                         "invalid volume specification",
    60  		"/path:rw":                        "invalid volume specification",
    61  		"/path:ro":                        "invalid volume specification",
    62  		"/rw:rw":                          "invalid volume specification",
    63  		"path:ro":                         "invalid volume specification",
    64  		"/path:/path:sw":                  `invalid mode`,
    65  		"/path:/path:rwz":                 `invalid mode`,
    66  		"/path:/path:ro,rshared,rslave":   `invalid mode`,
    67  		"/path:/path:ro,z,rshared,rslave": `invalid mode`,
    68  		"/path:shared":                    "invalid volume specification",
    69  		"/path:slave":                     "invalid volume specification",
    70  		"/path:private":                   "invalid volume specification",
    71  		"name:/absolute-path:shared":      "invalid volume specification",
    72  		"name:/absolute-path:rshared":     "invalid volume specification",
    73  		"name:/absolute-path:slave":       "invalid volume specification",
    74  		"name:/absolute-path:rslave":      "invalid volume specification",
    75  		"name:/absolute-path:private":     "invalid volume specification",
    76  		"name:/absolute-path:rprivate":    "invalid volume specification",
    77  	}
    78  
    79  	parser := NewLinuxParser()
    80  	if p, ok := parser.(*linuxParser); ok {
    81  		p.fi = mockFiProvider{}
    82  	}
    83  
    84  	for _, path := range valid {
    85  		if _, err := parser.ParseMountRaw(path, "local"); err != nil {
    86  			t.Errorf("ParseMountRaw(`%q`) should succeed: error %q", path, err)
    87  		}
    88  	}
    89  
    90  	for path, expectedError := range invalid {
    91  		if mp, err := parser.ParseMountRaw(path, "local"); err == nil {
    92  			t.Errorf("ParseMountRaw(`%q`) should have failed validation. Err '%v' - MP: %v", path, err, mp)
    93  		} else {
    94  			if !strings.Contains(err.Error(), expectedError) {
    95  				t.Errorf("ParseMountRaw(`%q`) error should contain %q, got %v", path, expectedError, err.Error())
    96  			}
    97  		}
    98  	}
    99  }
   100  
   101  func TestLinuxParseMountRawSplit(t *testing.T) {
   102  	cases := []struct {
   103  		bind      string
   104  		driver    string
   105  		expType   mount.Type
   106  		expDest   string
   107  		expSource string
   108  		expName   string
   109  		expDriver string
   110  		expRW     bool
   111  		fail      bool
   112  	}{
   113  		{
   114  			bind:      "/tmp:/tmp1",
   115  			expType:   mount.TypeBind,
   116  			expDest:   "/tmp1",
   117  			expSource: "/tmp",
   118  			expRW:     true,
   119  		},
   120  		{
   121  			bind:      "/tmp:/tmp2:ro",
   122  			expType:   mount.TypeBind,
   123  			expDest:   "/tmp2",
   124  			expSource: "/tmp",
   125  		},
   126  		{
   127  			bind:      "/tmp:/tmp3:rw",
   128  			expType:   mount.TypeBind,
   129  			expDest:   "/tmp3",
   130  			expSource: "/tmp",
   131  			expRW:     true,
   132  		},
   133  		{
   134  			bind:    "/tmp:/tmp4:foo",
   135  			expType: mount.TypeBind,
   136  			fail:    true,
   137  		},
   138  		{
   139  			bind:    "name:/named1",
   140  			expType: mount.TypeVolume,
   141  			expDest: "/named1",
   142  			expName: "name",
   143  			expRW:   true,
   144  		},
   145  		{
   146  			bind:      "name:/named2",
   147  			driver:    "external",
   148  			expType:   mount.TypeVolume,
   149  			expDest:   "/named2",
   150  			expName:   "name",
   151  			expDriver: "external",
   152  			expRW:     true,
   153  		},
   154  		{
   155  			bind:      "name:/named3:ro",
   156  			driver:    "local",
   157  			expType:   mount.TypeVolume,
   158  			expDest:   "/named3",
   159  			expName:   "name",
   160  			expDriver: "local",
   161  		},
   162  		{
   163  			bind:    "local/name:/tmp:rw",
   164  			expType: mount.TypeVolume,
   165  			expDest: "/tmp",
   166  			expName: "local/name",
   167  			expRW:   true,
   168  		},
   169  		{
   170  			bind:    "/tmp:tmp",
   171  			expType: mount.TypeBind,
   172  			expRW:   true,
   173  			fail:    true,
   174  		},
   175  	}
   176  
   177  	parser := NewLinuxParser()
   178  	if p, ok := parser.(*linuxParser); ok {
   179  		p.fi = mockFiProvider{}
   180  	}
   181  
   182  	for _, tc := range cases {
   183  		tc := tc
   184  		t.Run(tc.bind, func(t *testing.T) {
   185  			m, err := parser.ParseMountRaw(tc.bind, tc.driver)
   186  			if tc.fail {
   187  				assert.Check(t, is.ErrorContains(err, ""), "expected an error")
   188  				return
   189  			}
   190  
   191  			assert.NilError(t, err)
   192  			assert.Check(t, is.Equal(m.Destination, tc.expDest))
   193  			assert.Check(t, is.Equal(m.Source, tc.expSource))
   194  			assert.Check(t, is.Equal(m.Name, tc.expName))
   195  			assert.Check(t, is.Equal(m.Driver, tc.expDriver))
   196  			assert.Check(t, is.Equal(m.RW, tc.expRW))
   197  			assert.Check(t, is.Equal(m.Type, tc.expType))
   198  		})
   199  	}
   200  }
   201  
   202  // TestLinuxParseMountSpecBindWithFileinfoError makes sure that the parser returns
   203  // the error produced by the fileinfo provider.
   204  //
   205  // Some extra context for the future in case of changes and possible wtf are we
   206  // testing this for:
   207  //
   208  // Currently this "fileInfoProvider" returns (bool, bool, error)
   209  // The 1st bool is "does this path exist"
   210  // The 2nd bool is "is this path a dir"
   211  // Then of course the error is an error.
   212  //
   213  // The issue is the parser was ignoring the error and only looking at the
   214  // "does this path exist" boolean, which is always false if there is an error.
   215  // Then the error returned to the caller was a (slightly, maybe) friendlier
   216  // error string than what comes from `os.Stat`
   217  // So ...the caller was always getting an error saying the path doesn't exist
   218  // even if it does exist but got some other error (like a permission error).
   219  // This is confusing to users.
   220  func TestLinuxParseMountSpecBindWithFileinfoError(t *testing.T) {
   221  	parser := NewLinuxParser()
   222  	testErr := fmt.Errorf("some crazy error")
   223  	if pr, ok := parser.(*linuxParser); ok {
   224  		pr.fi = &mockFiProviderWithError{err: testErr}
   225  	}
   226  
   227  	_, err := parser.ParseMountSpec(mount.Mount{
   228  		Type:   mount.TypeBind,
   229  		Source: `/bananas`,
   230  		Target: `/bananas`,
   231  	})
   232  	assert.ErrorContains(t, err, testErr.Error())
   233  }
   234  
   235  func TestConvertTmpfsOptions(t *testing.T) {
   236  	type testCase struct {
   237  		opt                  mount.TmpfsOptions
   238  		readOnly             bool
   239  		expectedSubstrings   []string
   240  		unexpectedSubstrings []string
   241  	}
   242  	cases := []testCase{
   243  		{
   244  			opt:                  mount.TmpfsOptions{SizeBytes: 1024 * 1024, Mode: 0o700},
   245  			readOnly:             false,
   246  			expectedSubstrings:   []string{"size=1m", "mode=700"},
   247  			unexpectedSubstrings: []string{"ro"},
   248  		},
   249  		{
   250  			opt:                  mount.TmpfsOptions{},
   251  			readOnly:             true,
   252  			expectedSubstrings:   []string{"ro"},
   253  			unexpectedSubstrings: []string{},
   254  		},
   255  	}
   256  	p := NewLinuxParser()
   257  	for _, tc := range cases {
   258  		data, err := p.ConvertTmpfsOptions(&tc.opt, tc.readOnly)
   259  		if err != nil {
   260  			t.Fatalf("could not convert %+v (readOnly: %v) to string: %v",
   261  				tc.opt, tc.readOnly, err)
   262  		}
   263  		t.Logf("data=%q", data)
   264  		for _, s := range tc.expectedSubstrings {
   265  			if !strings.Contains(data, s) {
   266  				t.Fatalf("expected substring: %s, got %v (case=%+v)", s, data, tc)
   267  			}
   268  		}
   269  		for _, s := range tc.unexpectedSubstrings {
   270  			if strings.Contains(data, s) {
   271  				t.Fatalf("unexpected substring: %s, got %v (case=%+v)", s, data, tc)
   272  			}
   273  		}
   274  	}
   275  }