github.com/containerd/nerdctl@v1.7.7/pkg/mountutil/mountutil_linux_test.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package mountutil
    18  
    19  import (
    20  	"context"
    21  	"strings"
    22  	"testing"
    23  
    24  	"github.com/containerd/containerd/mount"
    25  	"github.com/containerd/containerd/oci"
    26  	"github.com/containerd/nerdctl/pkg/inspecttypes/native"
    27  	mocks "github.com/containerd/nerdctl/pkg/mountutil/mountutilmock"
    28  	"github.com/opencontainers/runtime-spec/specs-go"
    29  	"go.uber.org/mock/gomock"
    30  	"gotest.tools/v3/assert"
    31  	is "gotest.tools/v3/assert/cmp"
    32  )
    33  
    34  // TestParseVolumeOptions tests volume options are parsed as expected.
    35  func TestParseVolumeOptions(t *testing.T) {
    36  	tests := []struct {
    37  		name                     string
    38  		vType                    string
    39  		src                      string
    40  		optsRaw                  string
    41  		srcOptional              []string
    42  		initialRootfsPropagation string
    43  		wants                    []string
    44  		wantRootfsPropagation    string
    45  		wantFail                 bool
    46  	}{
    47  		{
    48  			name:    "unknown option is ignored (with warning)",
    49  			vType:   "volume",
    50  			src:     "dummy",
    51  			optsRaw: "ro,undefined",
    52  			wants:   []string{"ro"},
    53  		},
    54  
    55  		// tests for rw/ro flags
    56  		{
    57  			name:    "read write",
    58  			vType:   "bind",
    59  			src:     "dummy",
    60  			optsRaw: "rw",
    61  			wants:   []string{"rprivate"},
    62  		},
    63  		{
    64  			name:    "read only",
    65  			vType:   "volume",
    66  			src:     "dummy",
    67  			optsRaw: "ro",
    68  			wants:   []string{"ro"},
    69  		},
    70  		{
    71  			name:     "duplicated flags are not allowed",
    72  			vType:    "bind",
    73  			src:      "dummy",
    74  			optsRaw:  "ro,rw",
    75  			wantFail: true,
    76  		},
    77  		{
    78  			name:     "duplicated flags (ro/ro) are not allowed",
    79  			vType:    "volume",
    80  			src:      "dummy",
    81  			optsRaw:  "ro,ro",
    82  			wantFail: true,
    83  		},
    84  
    85  		// tests for propagation flags
    86  		{
    87  			name:     "volume doesn't accept propagation option",
    88  			vType:    "volume",
    89  			src:      "dummy",
    90  			optsRaw:  "private",
    91  			wantFail: true,
    92  		},
    93  		{
    94  			name:     "duplicated propagation option is not allowed",
    95  			vType:    "bind",
    96  			src:      "dummy",
    97  			optsRaw:  "private,shared",
    98  			wantFail: true,
    99  		},
   100  		{
   101  			name:  "default propagation type is rprivate",
   102  			vType: "bind",
   103  			src:   "dummy",
   104  			wants: []string{"rprivate"},
   105  		},
   106  		{
   107  			name:    "make bind private",
   108  			vType:   "bind",
   109  			src:     "dummy",
   110  			optsRaw: "ro,private",
   111  			wants:   []string{"ro", "private"},
   112  		},
   113  		{
   114  			name:    "make bind nonrecursive",
   115  			vType:   "bind",
   116  			src:     "dummy",
   117  			optsRaw: "bind",
   118  			wants:   []string{"bind", "rprivate"},
   119  		},
   120  		{
   121  			name:                  "make bind shared",
   122  			vType:                 "bind",
   123  			src:                   "dummy",
   124  			optsRaw:               "ro,rshared",
   125  			srcOptional:           []string{"shared:xxx"},
   126  			wantRootfsPropagation: "shared",
   127  			wants:                 []string{"ro", "rshared"},
   128  		},
   129  		{
   130  			name:                     "make bind shared (unchange RootfsPropagation)",
   131  			vType:                    "bind",
   132  			src:                      "dummy",
   133  			optsRaw:                  "ro,rshared",
   134  			srcOptional:              []string{"shared:xxx"},
   135  			initialRootfsPropagation: "rshared",
   136  			wantRootfsPropagation:    "rshared",
   137  			wants:                    []string{"ro", "rshared"},
   138  		},
   139  		{
   140  			name:        "shared propagation is not allowed if the src is not shared",
   141  			vType:       "bind",
   142  			src:         "dummy",
   143  			optsRaw:     "ro,shared",
   144  			srcOptional: nil,
   145  			wantFail:    true,
   146  		},
   147  		{
   148  			name:                  "make bind slave",
   149  			vType:                 "bind",
   150  			src:                   "dummy",
   151  			optsRaw:               "ro,slave",
   152  			srcOptional:           []string{"master:xxx"},
   153  			wantRootfsPropagation: "rslave",
   154  			wants:                 []string{"ro", "slave"},
   155  		},
   156  		{
   157  			name:                     "make bind slave (unchange RootfsPropagation)",
   158  			vType:                    "bind",
   159  			src:                      "dummy",
   160  			optsRaw:                  "ro,slave",
   161  			srcOptional:              []string{"master:xxx"},
   162  			initialRootfsPropagation: "shared",
   163  			wantRootfsPropagation:    "shared",
   164  			wants:                    []string{"ro", "slave"},
   165  		},
   166  		{
   167  			name:        "slave propagation is not allowed if the src is not slave",
   168  			vType:       "bind",
   169  			src:         "dummy",
   170  			optsRaw:     "ro,slave",
   171  			srcOptional: nil,
   172  			wantFail:    true,
   173  		},
   174  	}
   175  	for _, tt := range tests {
   176  		t.Run(tt.name, func(t *testing.T) {
   177  			opts, specOpts, err := parseVolumeOptionsWithMountInfo(tt.vType, tt.src, tt.optsRaw, func(string) (mount.Info, error) {
   178  				return mount.Info{
   179  					Mountpoint: tt.src,
   180  					Optional:   strings.Join(tt.srcOptional, " "),
   181  				}, nil
   182  			})
   183  			if err != nil {
   184  				if tt.wantFail {
   185  					return
   186  				}
   187  				t.Errorf("failed to parse option %q: %v", tt.optsRaw, err)
   188  				return
   189  			}
   190  			s := oci.Spec{Linux: &specs.Linux{RootfsPropagation: tt.initialRootfsPropagation}}
   191  			for _, o := range specOpts {
   192  				assert.NilError(t, o(context.Background(), nil, nil, &s))
   193  			}
   194  			assert.Equal(t, tt.wantRootfsPropagation, s.Linux.RootfsPropagation)
   195  			assert.Equal(t, tt.wantFail, false)
   196  			assert.Check(t, is.DeepEqual(tt.wants, opts))
   197  		})
   198  	}
   199  }
   200  
   201  func TestProcessTmpfs(t *testing.T) {
   202  	testCases := map[string][]string{
   203  		"/tmp":               {"noexec", "nosuid", "nodev"},
   204  		"/tmp:size=64m,exec": {"nosuid", "nodev", "size=64m", "exec"},
   205  	}
   206  	for k, expected := range testCases {
   207  		x, err := ProcessFlagTmpfs(k)
   208  		assert.NilError(t, err)
   209  		assert.DeepEqual(t, expected, x.Mount.Options)
   210  	}
   211  }
   212  
   213  func TestProcessFlagV(t *testing.T) {
   214  	tests := []struct {
   215  		rawSpec string
   216  		wants   *Processed
   217  		err     string
   218  	}{
   219  		// Bind volumes: absolute path
   220  		{
   221  			rawSpec: "/mnt/foo:/mnt/foo:ro",
   222  			wants: &Processed{
   223  				Type: "bind",
   224  				Mount: specs.Mount{
   225  					Type:        "none",
   226  					Destination: `/mnt/foo`,
   227  					Source:      `/mnt/foo`,
   228  					Options:     []string{"ro", "rprivate", "rbind"},
   229  				}},
   230  		},
   231  		// Bind volumes: relative path
   232  		{
   233  			rawSpec: `./TestVolume/Path:/mnt/foo`,
   234  			wants: &Processed{
   235  				Type: "bind",
   236  				Mount: specs.Mount{
   237  					Type:        "none",
   238  					Source:      "", // will not check source of relative paths
   239  					Destination: `/mnt/foo`,
   240  					Options:     []string{"rbind"},
   241  				}},
   242  		},
   243  		// Named volumes
   244  		{
   245  			rawSpec: `TestVolume:/mnt/foo`,
   246  			wants: &Processed{
   247  				Type: "volume",
   248  				Name: "TestVolume",
   249  				Mount: specs.Mount{
   250  					Type:        "none",
   251  					Source:      "", // source of anonymous volume is a generated path, so here will not check it.
   252  					Destination: `/mnt/foo`,
   253  					Options:     []string{"rbind"},
   254  				}},
   255  		},
   256  		{
   257  			rawSpec: `/mnt/foo:TestVolume`,
   258  			err:     "expected an absolute path, got \"TestVolume\"",
   259  		},
   260  		{
   261  			rawSpec: `/mnt/foo:./foo`,
   262  			err:     "expected an absolute path, got \"./foo\"",
   263  		},
   264  	}
   265  
   266  	ctrl := gomock.NewController(t)
   267  	defer ctrl.Finish()
   268  
   269  	mockVolumeStore := mocks.NewMockVolumeStore(ctrl)
   270  	mockVolumeStore.
   271  		EXPECT().
   272  		Get(gomock.Any(), false).
   273  		Return(&native.Volume{Name: "test_volume", Mountpoint: "/test/volume", Size: 1024}, nil).
   274  		AnyTimes()
   275  	mockVolumeStore.
   276  		EXPECT().
   277  		Create(gomock.Any(), nil).
   278  		Return(&native.Volume{Name: "test_volume", Mountpoint: "/test/volume"}, nil).AnyTimes()
   279  
   280  	mockOs := mocks.NewMockOs(ctrl)
   281  	mockOs.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
   282  
   283  	for _, tt := range tests {
   284  		t.Run(tt.rawSpec, func(t *testing.T) {
   285  			processedVolSpec, err := ProcessFlagV(tt.rawSpec, mockVolumeStore, false)
   286  			if err != nil {
   287  				assert.Error(t, err, tt.err)
   288  				return
   289  			}
   290  
   291  			assert.Equal(t, processedVolSpec.Type, tt.wants.Type)
   292  			assert.Equal(t, processedVolSpec.Mount.Type, tt.wants.Mount.Type)
   293  			assert.Equal(t, processedVolSpec.Mount.Destination, tt.wants.Mount.Destination)
   294  			assert.DeepEqual(t, processedVolSpec.Mount.Options, tt.wants.Mount.Options)
   295  
   296  			if tt.wants.Name != "" {
   297  				assert.Equal(t, processedVolSpec.Name, tt.wants.Name)
   298  			}
   299  			if tt.wants.Mount.Source != "" {
   300  				assert.Equal(t, processedVolSpec.Mount.Source, tt.wants.Mount.Source)
   301  			}
   302  		})
   303  	}
   304  }
   305  
   306  func TestProcessFlagVAnonymousVolumes(t *testing.T) {
   307  	tests := []struct {
   308  		rawSpec string
   309  		wants   *Processed
   310  		err     string
   311  	}{
   312  		{
   313  			rawSpec: `/mnt/foo`,
   314  			wants: &Processed{
   315  				Type: "volume",
   316  				Mount: specs.Mount{
   317  					Type:        "none",
   318  					Source:      "", // source of anonymous volume is a generated path, so here will not check it.
   319  					Destination: `/mnt/foo`,
   320  				}},
   321  		},
   322  		{
   323  			rawSpec: `./TestVolume/Path`,
   324  			wants: &Processed{
   325  				Type: "volume",
   326  				Mount: specs.Mount{
   327  					Type:        "none",
   328  					Source:      "",                // source of anonymous volume is a generated path, so here will not check it.
   329  					Destination: `TestVolume/Path`, // cleanpath() removes the leading "./". Since we are mocking the os.Stat() call, this is fine.
   330  				}},
   331  		},
   332  		{
   333  			rawSpec: "TestVolume",
   334  			wants: &Processed{
   335  				Type: "volume",
   336  				Mount: specs.Mount{
   337  					Type:        "none",
   338  					Source:      "", // source of anonymous volume is a generated path, so here will not check it.
   339  					Destination: "TestVolume",
   340  				}},
   341  		},
   342  		{
   343  			rawSpec: `/mnt/foo::ro`,
   344  			err:     "expected an absolute path, got \"\"",
   345  		},
   346  	}
   347  
   348  	ctrl := gomock.NewController(t)
   349  	defer ctrl.Finish()
   350  
   351  	mockVolumeStore := mocks.NewMockVolumeStore(ctrl)
   352  	mockVolumeStore.
   353  		EXPECT().
   354  		Create(gomock.Any(), []string{}).
   355  		Return(&native.Volume{Name: "test_volume", Mountpoint: "/test/volume"}, nil).
   356  		AnyTimes()
   357  
   358  	for _, tt := range tests {
   359  		t.Run(tt.rawSpec, func(t *testing.T) {
   360  			processedVolSpec, err := ProcessFlagV(tt.rawSpec, mockVolumeStore, true)
   361  			if err != nil {
   362  				assert.ErrorContains(t, err, tt.err)
   363  				return
   364  			}
   365  
   366  			assert.Equal(t, processedVolSpec.Type, tt.wants.Type)
   367  			assert.Assert(t, processedVolSpec.AnonymousVolume != "")
   368  			assert.Equal(t, processedVolSpec.Mount.Type, tt.wants.Mount.Type)
   369  			assert.Equal(t, processedVolSpec.Mount.Destination, tt.wants.Mount.Destination)
   370  
   371  			if tt.wants.Mount.Source != "" {
   372  				assert.Equal(t, processedVolSpec.Mount.Source, tt.wants.Mount.Source)
   373  			}
   374  
   375  			// for anonymous volumes, we want to make sure that the source is not the same as the destination
   376  			assert.Assert(t, processedVolSpec.Mount.Source != processedVolSpec.Mount.Destination)
   377  		})
   378  	}
   379  }