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 }