github.com/containerd/nerdctl@v1.7.7/pkg/mountutil/mountutil_windows_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  	"fmt"
    21  	"strings"
    22  	"testing"
    23  
    24  	"github.com/containerd/nerdctl/pkg/inspecttypes/native"
    25  	mocks "github.com/containerd/nerdctl/pkg/mountutil/mountutilmock"
    26  	"github.com/opencontainers/runtime-spec/specs-go"
    27  	"go.uber.org/mock/gomock"
    28  	"gotest.tools/v3/assert"
    29  	is "gotest.tools/v3/assert/cmp"
    30  )
    31  
    32  func TestParseVolumeOptions(t *testing.T) {
    33  	tests := []struct {
    34  		vType    string
    35  		src      string
    36  		optsRaw  string
    37  		wants    []string
    38  		wantFail bool
    39  	}{
    40  		{
    41  			vType:   "bind",
    42  			src:     "dummy",
    43  			optsRaw: "rw",
    44  			wants:   []string{},
    45  		},
    46  		{
    47  			vType:   "volume",
    48  			src:     "dummy",
    49  			optsRaw: "ro",
    50  			wants:   []string{"ro"},
    51  		},
    52  		{
    53  			vType:   "volume",
    54  			src:     "dummy",
    55  			optsRaw: "ro,undefined",
    56  			wants:   []string{"ro"},
    57  		},
    58  		{
    59  			vType:    "bind",
    60  			src:      "dummy",
    61  			optsRaw:  "ro,rw",
    62  			wantFail: true,
    63  		},
    64  		{
    65  			vType:    "volume",
    66  			src:      "dummy",
    67  			optsRaw:  "ro,ro",
    68  			wantFail: true,
    69  		},
    70  	}
    71  	for _, tt := range tests {
    72  		t.Run(strings.Join([]string{tt.vType, tt.src, tt.optsRaw}, "-"), func(t *testing.T) {
    73  			opts, _, err := parseVolumeOptions(tt.vType, tt.src, tt.optsRaw)
    74  			if err != nil {
    75  				if tt.wantFail {
    76  					return
    77  				}
    78  				t.Errorf("failed to parse option %q: %v", tt.optsRaw, err)
    79  				return
    80  			}
    81  			assert.Equal(t, tt.wantFail, false)
    82  			assert.Check(t, is.DeepEqual(tt.wants, opts))
    83  		})
    84  	}
    85  }
    86  
    87  func TestSplitRawSpec(t *testing.T) {
    88  	tests := []struct {
    89  		rawSpec string
    90  		wants   []string
    91  	}{
    92  		// Absolute paths
    93  		{
    94  			rawSpec: `C:\TestVolume\Path:C:\TestVolume\Path:ro`,
    95  			wants:   []string{`C:\TestVolume\Path`, `C:\TestVolume\Path`, "ro"},
    96  		},
    97  		{
    98  			rawSpec: `C:\TestVolume\Path:C:\TestVolume\Path:ro,rw`,
    99  			wants:   []string{`C:\TestVolume\Path`, `C:\TestVolume\Path`, "ro,rw"},
   100  		},
   101  		{
   102  			rawSpec: `C:\TestVolume\Path:C:\TestVolume\Path:ro,undefined`,
   103  			wants:   []string{`C:\TestVolume\Path`, `C:\TestVolume\Path`, "ro,undefined"},
   104  		},
   105  		{
   106  			rawSpec: `C:\TestVolume\Path:C:\TestVolume\Path`,
   107  			wants:   []string{`C:\TestVolume\Path`, `C:\TestVolume\Path`},
   108  		},
   109  		{
   110  			rawSpec: `C:\TestVolume\Path`,
   111  			wants:   []string{`C:\TestVolume\Path`},
   112  		},
   113  		{
   114  			rawSpec: `C:\Test Volume\Path`, // space in path
   115  			wants:   []string{`C:\Test Volume\Path`},
   116  		},
   117  
   118  		// Relative paths
   119  		{
   120  			rawSpec: `.\ContainerVolumes:C:\TestVolumes`,
   121  			wants:   []string{`.\ContainerVolumes`, `C:\TestVolumes`},
   122  		},
   123  		{
   124  			rawSpec: `.\ContainerVolumes:.\ContainerVolumes`,
   125  			wants:   []string{`.\ContainerVolumes`, `.\ContainerVolumes`},
   126  		},
   127  
   128  		// Anonymous volumes
   129  		{
   130  			rawSpec: `.\ContainerVolumes`,
   131  			wants:   []string{`.\ContainerVolumes`},
   132  		},
   133  		{
   134  			rawSpec: `TestVolume`,
   135  			wants:   []string{`TestVolume`},
   136  		},
   137  		{
   138  			rawSpec: `:TestVolume`,
   139  			wants:   []string{`TestVolume`},
   140  		},
   141  
   142  		// UNC paths
   143  		{
   144  			rawSpec: `\\?\UNC\server\share\path:.\ContainerVolumesto`,
   145  			wants:   []string{`\\?\UNC\server\share\path`, `.\ContainerVolumesto`},
   146  		},
   147  		{
   148  			rawSpec: `\\.\Volume{b75e2c83-0000-0000-0000-602f00000000}\Test`,
   149  			wants:   []string{`\\.\Volume{b75e2c83-0000-0000-0000-602f00000000}\Test`},
   150  		},
   151  
   152  		// Named pipes
   153  		{
   154  			rawSpec: `\\.\pipe\containerd-containerd`,
   155  			wants:   []string{`\\.\pipe\containerd-containerd`},
   156  		},
   157  		{
   158  			rawSpec: `\\.\pipe\containerd-containerd:\\.\pipe\containerd-containerd`,
   159  			wants:   []string{`\\.\pipe\containerd-containerd`, `\\.\pipe\containerd-containerd`},
   160  		},
   161  	}
   162  	for _, tt := range tests {
   163  		t.Run(tt.rawSpec, func(t *testing.T) {
   164  			actual, err := splitVolumeSpec(tt.rawSpec)
   165  			if err != nil {
   166  				t.Errorf("failed to split raw spec %q: %v", tt.rawSpec, err)
   167  				return
   168  
   169  			}
   170  			assert.Check(t, is.DeepEqual(tt.wants, actual))
   171  		})
   172  	}
   173  }
   174  
   175  func TestSplitRawSpecInvalid(t *testing.T) {
   176  	tests := []string{
   177  		"",                                     // Empty string
   178  		"   ",                                  // Empty string
   179  		`.`,                                    // Invalid relative path
   180  		`./`,                                   // Invalid relative path
   181  		`../`,                                  // Invalid relative path
   182  		`C:\`,                                  // Cannot mount root directory
   183  		`~\TestVolume`,                         // Invalid relative path
   184  		`..\TestVolume`,                        // Invalid relative path
   185  		`ABC:\ContainerVolumes:C:\TestVolumes`, // Invalid drive letter
   186  		`UNC\server\share\path`,                // Invalid path
   187  	}
   188  
   189  	for _, path := range tests {
   190  		t.Run(path, func(t *testing.T) {
   191  			_, err := splitVolumeSpec(path)
   192  			if strings.TrimSpace(path) == "" {
   193  				assert.Error(t, err, "invalid empty volume specification")
   194  				return
   195  			}
   196  			if path == "." {
   197  				assert.Error(t, err, "invalid volume specification: \".\"")
   198  				return
   199  			}
   200  			assert.Error(t, err, fmt.Sprintf("invalid volume specification: '%s'", path))
   201  		})
   202  	}
   203  }
   204  
   205  func TestProcessFlagV(t *testing.T) {
   206  	tests := []struct {
   207  		rawSpec string
   208  		wants   *Processed
   209  		err     string
   210  	}{
   211  		// Bind volumes: absolute path
   212  		{
   213  			rawSpec: "C:/TestVolume/Path:C:/TestVolume/Path:ro",
   214  			wants: &Processed{
   215  				Type: "bind",
   216  				Mount: specs.Mount{
   217  					Type:        "",
   218  					Destination: `C:\TestVolume\Path`,
   219  					Source:      `C:\TestVolume\Path`,
   220  					Options:     []string{"ro", "rbind"},
   221  				}},
   222  		},
   223  		// Bind volumes: relative path
   224  		{
   225  			rawSpec: `.\TestVolume\Path:C:\TestVolume\Path`,
   226  			wants: &Processed{
   227  				Type: "bind",
   228  				Mount: specs.Mount{
   229  					Type:        "",
   230  					Source:      "", // will not check source of relative paths
   231  					Destination: `C:\TestVolume\Path`,
   232  					Options:     []string{"rbind"},
   233  				}},
   234  		},
   235  		// Named volumes
   236  		{
   237  			rawSpec: `TestVolume:C:\TestVolume\Path`,
   238  			wants: &Processed{
   239  				Type: "volume",
   240  				Name: "TestVolume",
   241  				Mount: specs.Mount{
   242  					Type:        "",
   243  					Source:      "", // source of anonymous volume is a generated path, so here will not check it.
   244  					Destination: `C:\TestVolume\Path`,
   245  					Options:     []string{"rbind"},
   246  				}},
   247  		},
   248  		// Named pipes
   249  		{
   250  			rawSpec: `\\.\pipe\containerd-containerd:\\.\pipe\containerd-containerd`,
   251  			wants: &Processed{
   252  				Type: "npipe",
   253  				Mount: specs.Mount{
   254  					Type:        "",
   255  					Source:      `\\.\pipe\containerd-containerd`,
   256  					Destination: `\\.\pipe\containerd-containerd`,
   257  					Options:     []string{"rbind"},
   258  				}},
   259  		},
   260  		{
   261  			rawSpec: `\\.\pipe\containerd-containerd:C:\TestVolume\Path`,
   262  			err:     "invalid volume specification. named pipes can only be mapped to named pipes",
   263  		},
   264  		{
   265  			rawSpec: `C:\TestVolume\Path:TestVolume`,
   266  			err:     "expected an absolute path or a named pipe, got \"TestVolume\"",
   267  		},
   268  	}
   269  
   270  	ctrl := gomock.NewController(t)
   271  	defer ctrl.Finish()
   272  
   273  	mockVolumeStore := mocks.NewMockVolumeStore(ctrl)
   274  	mockVolumeStore.
   275  		EXPECT().
   276  		Get(gomock.Any(), false).
   277  		Return(&native.Volume{Name: "test_volume", Mountpoint: "C:\\test\\directory", Size: 1024}, nil).
   278  		AnyTimes()
   279  	mockVolumeStore.
   280  		EXPECT().
   281  		Create(gomock.Any(), nil).
   282  		Return(&native.Volume{Name: "test_volume", Mountpoint: "C:\\test\\directory"}, nil).AnyTimes()
   283  
   284  	mockOs := mocks.NewMockOs(ctrl)
   285  	mockOs.EXPECT().Stat(gomock.Any()).Return(nil, nil).AnyTimes()
   286  
   287  	for _, tt := range tests {
   288  		t.Run(tt.rawSpec, func(t *testing.T) {
   289  			processedVolSpec, err := ProcessFlagV(tt.rawSpec, mockVolumeStore, true)
   290  			if err != nil {
   291  				assert.Error(t, err, tt.err)
   292  				return
   293  			}
   294  
   295  			assert.Equal(t, processedVolSpec.Type, tt.wants.Type)
   296  			assert.Equal(t, processedVolSpec.Mount.Type, tt.wants.Mount.Type)
   297  			assert.Equal(t, processedVolSpec.Mount.Destination, tt.wants.Mount.Destination)
   298  			assert.DeepEqual(t, processedVolSpec.Mount.Options, tt.wants.Mount.Options)
   299  
   300  			if tt.wants.Name != "" {
   301  				assert.Equal(t, processedVolSpec.Name, tt.wants.Name)
   302  			}
   303  			if tt.wants.Mount.Source != "" {
   304  				assert.Equal(t, processedVolSpec.Mount.Source, tt.wants.Mount.Source)
   305  			}
   306  		})
   307  	}
   308  }
   309  
   310  func TestProcessFlagVAnonymousVolumes(t *testing.T) {
   311  	tests := []struct {
   312  		rawSpec string
   313  		wants   *Processed
   314  		err     string
   315  	}{
   316  		{
   317  			rawSpec: `C:\TestVolume\Path`,
   318  			wants: &Processed{
   319  				Type: "volume",
   320  				Mount: specs.Mount{
   321  					Type:        "",
   322  					Source:      "", // source of anonymous volume is a generated path, so here will not check it.
   323  					Destination: `C:\TestVolume\Path`,
   324  				}},
   325  		},
   326  		{
   327  			rawSpec: `.\TestVolume\Path`,
   328  			err:     "expected an absolute path",
   329  		},
   330  		{
   331  			rawSpec: `TestVolume`,
   332  			err:     "only directories can be mapped as anonymous volumes",
   333  		},
   334  		{
   335  			rawSpec: `C:\TestVolume\Path::ro`,
   336  			err:     "failed to split volume mount specification",
   337  		},
   338  		{
   339  			rawSpec: `\\.\pipe\containerd-containerd`,
   340  			err:     "only directories can be mapped as anonymous volumes",
   341  		},
   342  	}
   343  
   344  	ctrl := gomock.NewController(t)
   345  	defer ctrl.Finish()
   346  
   347  	mockVolumeStore := mocks.NewMockVolumeStore(ctrl)
   348  	mockVolumeStore.
   349  		EXPECT().
   350  		Create(gomock.Any(), []string{}).
   351  		Return(&native.Volume{Name: "test_volume", Mountpoint: "C:\\test\\directory"}, nil).
   352  		AnyTimes()
   353  
   354  	for _, tt := range tests {
   355  		t.Run(tt.rawSpec, func(t *testing.T) {
   356  			processedVolSpec, err := ProcessFlagV(tt.rawSpec, mockVolumeStore, true)
   357  			if err != nil {
   358  				assert.ErrorContains(t, err, tt.err)
   359  				return
   360  			}
   361  
   362  			assert.Equal(t, processedVolSpec.Type, tt.wants.Type)
   363  			assert.Assert(t, processedVolSpec.AnonymousVolume != "")
   364  			assert.Equal(t, processedVolSpec.Mount.Type, tt.wants.Mount.Type)
   365  			assert.Equal(t, processedVolSpec.Mount.Destination, tt.wants.Mount.Destination)
   366  
   367  			if tt.wants.Mount.Source != "" {
   368  				assert.Equal(t, processedVolSpec.Mount.Source, tt.wants.Mount.Source)
   369  			}
   370  
   371  			// for anonymous volumes, we want to make sure that the source is not the same as the destination
   372  			assert.Assert(t, processedVolSpec.Mount.Source != processedVolSpec.Mount.Destination)
   373  		})
   374  	}
   375  }