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 }