gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/runsc/boot/mount_hints_test.go (about) 1 // Copyright 2022 The gVisor Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package boot 16 17 import ( 18 "slices" 19 "strings" 20 "testing" 21 22 specs "github.com/opencontainers/runtime-spec/specs-go" 23 "gvisor.dev/gvisor/pkg/sentry/fsimpl/erofs" 24 "gvisor.dev/gvisor/runsc/config" 25 ) 26 27 func TestPodMountHintsHappy(t *testing.T) { 28 spec := &specs.Spec{ 29 Annotations: map[string]string{ 30 MountPrefix + "mount1.source": "foo", 31 MountPrefix + "mount1.type": "tmpfs", 32 MountPrefix + "mount1.share": "pod", 33 34 MountPrefix + "mount2.source": "bar", 35 MountPrefix + "mount2.type": "bind", 36 MountPrefix + "mount2.share": "container", 37 MountPrefix + "mount2.options": "rw,private", 38 }, 39 } 40 podHints, err := NewPodMountHints(spec) 41 if err != nil { 42 t.Fatalf("newPodMountHints failed: %v", err) 43 } 44 45 // Check that fields were set correctly. 46 mount1 := podHints.Mounts["mount1"] 47 if want := "mount1"; want != mount1.Name { 48 t.Errorf("mount1 name, want: %q, got: %q", want, mount1.Name) 49 } 50 if want := "foo"; want != mount1.Mount.Source { 51 t.Errorf("mount1 source, want: %q, got: %q", want, mount1.Mount.Source) 52 } 53 if want := "tmpfs"; want != mount1.Mount.Type { 54 t.Errorf("mount1 type, want: %q, got: %q", want, mount1.Mount.Type) 55 } 56 if want := pod; want != mount1.Share { 57 t.Errorf("mount1 type, want: %q, got: %q", want, mount1.Share) 58 } 59 if want := []string(nil); !slices.Equal(want, mount1.Mount.Options) { 60 t.Errorf("mount1 type, want: %q, got: %q", want, mount1.Mount.Options) 61 } 62 63 mount2 := podHints.Mounts["mount2"] 64 if want := "mount2"; want != mount2.Name { 65 t.Errorf("mount2 name, want: %q, got: %q", want, mount2.Name) 66 } 67 if want := "bar"; want != mount2.Mount.Source { 68 t.Errorf("mount2 source, want: %q, got: %q", want, mount2.Mount.Source) 69 } 70 if want := "bind"; want != mount2.Mount.Type { 71 t.Errorf("mount2 type, want: %q, got: %q", want, mount2.Mount.Type) 72 } 73 if want := container; want != mount2.Share { 74 t.Errorf("mount2 type, want: %q, got: %q", want, mount2.Share) 75 } 76 if want := []string{"rw", "private"}; !slices.Equal(want, mount2.Mount.Options) { 77 t.Errorf("mount2 type, want: %q, got: %q", want, mount2.Mount.Options) 78 } 79 } 80 81 func TestPodMountHintsErrors(t *testing.T) { 82 for _, tst := range []struct { 83 name string 84 annotations map[string]string 85 error string 86 }{ 87 { 88 name: "too short", 89 annotations: map[string]string{ 90 MountPrefix + "mount1": "foo", 91 }, 92 error: "invalid mount annotation", 93 }, 94 { 95 name: "no name", 96 annotations: map[string]string{ 97 MountPrefix + ".source": "foo", 98 }, 99 error: "invalid mount name", 100 }, 101 { 102 name: "duplicate source", 103 annotations: map[string]string{ 104 MountPrefix + "mount1.source": "foo", 105 MountPrefix + "mount1.type": "tmpfs", 106 MountPrefix + "mount1.share": "pod", 107 108 MountPrefix + "mount2.source": "foo", 109 MountPrefix + "mount2.type": "bind", 110 MountPrefix + "mount2.share": "container", 111 }, 112 error: "have the same mount source", 113 }, 114 } { 115 t.Run(tst.name, func(t *testing.T) { 116 spec := &specs.Spec{Annotations: tst.annotations} 117 podHints, err := NewPodMountHints(spec) 118 if err == nil || !strings.Contains(err.Error(), tst.error) { 119 t.Errorf("newPodMountHints invalid error, want: .*%s.*, got: %v", tst.error, err) 120 } 121 if podHints != nil { 122 t.Errorf("newPodMountHints must return nil on failure: %+v", podHints) 123 } 124 }) 125 } 126 } 127 128 // Tests that when a required mount annotation is missing, the entire mount 129 // hint is omitted and ignored. 130 func TestPodMountHintsIgnore(t *testing.T) { 131 for _, tst := range []struct { 132 name string 133 annotations map[string]string 134 }{ 135 { 136 name: "invalid source", 137 annotations: map[string]string{ 138 MountPrefix + "mount1.source": "", 139 MountPrefix + "mount1.type": "tmpfs", 140 MountPrefix + "mount1.share": "pod", 141 }, 142 }, 143 { 144 name: "invalid type", 145 annotations: map[string]string{ 146 MountPrefix + "mount1.source": "foo", 147 MountPrefix + "mount1.type": "invalid", 148 MountPrefix + "mount1.share": "pod", 149 }, 150 }, 151 { 152 name: "invalid share", 153 annotations: map[string]string{ 154 MountPrefix + "mount1.source": "foo", 155 MountPrefix + "mount1.type": "tmpfs", 156 MountPrefix + "mount1.share": "invalid", 157 }, 158 }, 159 } { 160 t.Run(tst.name, func(t *testing.T) { 161 spec := &specs.Spec{Annotations: tst.annotations} 162 podHints, err := NewPodMountHints(spec) 163 if err != nil { 164 t.Errorf("newPodMountHints() failed: %v", err) 165 } else if podHints != nil { 166 if hint, ok := podHints.Mounts["mount1"]; ok { 167 t.Errorf("hint was provided when it should have been omitted: %+v", hint) 168 } 169 } 170 }) 171 } 172 } 173 174 func TestIgnoreInvalidMountOptions(t *testing.T) { 175 spec := &specs.Spec{ 176 Annotations: map[string]string{ 177 MountPrefix + "mount1.source": "foo", 178 MountPrefix + "mount1.type": "tmpfs", 179 MountPrefix + "mount1.share": "container", 180 MountPrefix + "mount1.options": "rw,shared,noexec", 181 }, 182 } 183 podHints, err := NewPodMountHints(spec) 184 if err != nil { 185 t.Fatalf("newPodMountHints failed: %v", err) 186 } 187 mount1 := podHints.Mounts["mount1"] 188 if want := []string{"rw", "noexec"}; !slices.Equal(want, mount1.Mount.Options) { 189 t.Errorf("mount2 type, want: %q, got: %q", want, mount1.Mount.Options) 190 } 191 } 192 193 func TestHintsCheckCompatible(t *testing.T) { 194 for _, tc := range []struct { 195 name string 196 masterOpts []string 197 replicaOpts []string 198 err string 199 }{ 200 { 201 name: "empty", 202 }, 203 { 204 name: "same", 205 masterOpts: []string{"ro", "noatime", "noexec"}, 206 replicaOpts: []string{"ro", "noatime", "noexec"}, 207 }, 208 { 209 name: "compatible", 210 masterOpts: []string{"rw", "atime", "exec"}, 211 replicaOpts: []string{"ro", "noatime", "noexec"}, 212 }, 213 { 214 name: "unsupported", 215 masterOpts: []string{"nofoo", "nodev"}, 216 replicaOpts: []string{"foo", "dev"}, 217 }, 218 { 219 name: "incompatible-ro", 220 masterOpts: []string{"ro"}, 221 replicaOpts: []string{"rw"}, 222 err: "read-write", 223 }, 224 { 225 name: "incompatible-atime", 226 masterOpts: []string{"noatime"}, 227 replicaOpts: []string{"atime"}, 228 err: "noatime", 229 }, 230 { 231 name: "incompatible-exec", 232 masterOpts: []string{"noexec"}, 233 replicaOpts: []string{"exec"}, 234 err: "noexec", 235 }, 236 } { 237 t.Run(tc.name, func(t *testing.T) { 238 master := MountHint{Mount: specs.Mount{Options: tc.masterOpts}} 239 replica := specs.Mount{Options: tc.replicaOpts} 240 if err := master.checkCompatible(&replica); err != nil { 241 if !strings.Contains(err.Error(), tc.err) { 242 t.Fatalf("wrong error, want: %q, got: %q", tc.err, err) 243 } 244 } else { 245 if len(tc.err) > 0 { 246 t.Fatalf("error %q expected", tc.err) 247 } 248 } 249 }) 250 } 251 } 252 253 // TestRootfsHintHappy tests that valid rootfs annotations can be parsed correctly. 254 func TestRootfsHintHappy(t *testing.T) { 255 const imagePath = "/tmp/rootfs.img" 256 spec := &specs.Spec{ 257 Annotations: map[string]string{ 258 RootfsPrefix + "source": imagePath, 259 RootfsPrefix + "type": erofs.Name, 260 RootfsPrefix + "overlay": config.MemoryOverlay.String(), 261 }, 262 } 263 hint, err := NewRootfsHint(spec) 264 if err != nil { 265 t.Fatalf("NewRootfsHint failed: %v", err) 266 } 267 268 // Check that fields were set correctly. 269 if hint.Mount.Source != imagePath { 270 t.Errorf("rootfs source, want: %q, got: %q", imagePath, hint.Mount.Source) 271 } 272 if hint.Mount.Type != erofs.Name { 273 t.Errorf("rootfs type, want: %q, got: %q", erofs.Name, hint.Mount.Type) 274 } 275 if hint.Overlay != config.MemoryOverlay { 276 t.Errorf("rootfs overlay, want: %q, got: %q", config.MemoryOverlay, hint.Overlay) 277 } 278 } 279 280 // TestRootfsHintErrors tests that proper errors will be returned when parsing 281 // invalid rootfs annotations. 282 func TestRootfsHintErrors(t *testing.T) { 283 const imagePath = "/tmp/rootfs.img" 284 for _, tst := range []struct { 285 name string 286 annotations map[string]string 287 error string 288 }{ 289 { 290 name: "invalid source", 291 annotations: map[string]string{ 292 RootfsPrefix + "source": "invalid", 293 RootfsPrefix + "type": erofs.Name, 294 }, 295 error: "invalid rootfs annotation", 296 }, 297 { 298 name: "invalid type", 299 annotations: map[string]string{ 300 RootfsPrefix + "source": imagePath, 301 RootfsPrefix + "type": "invalid", 302 }, 303 error: "invalid rootfs annotation", 304 }, 305 { 306 name: "invalid overlay", 307 annotations: map[string]string{ 308 RootfsPrefix + "source": imagePath, 309 RootfsPrefix + "type": erofs.Name, 310 RootfsPrefix + "overlay": "invalid", 311 }, 312 error: "invalid rootfs annotation", 313 }, 314 { 315 name: "invalid key", 316 annotations: map[string]string{ 317 RootfsPrefix + "invalid": "invalid", 318 RootfsPrefix + "source": imagePath, 319 RootfsPrefix + "type": erofs.Name, 320 RootfsPrefix + "overlay": config.MemoryOverlay.String(), 321 }, 322 error: "invalid rootfs annotation", 323 }, 324 { 325 name: "missing source", 326 annotations: map[string]string{ 327 RootfsPrefix + "type": erofs.Name, 328 RootfsPrefix + "overlay": config.MemoryOverlay.String(), 329 }, 330 error: "rootfs annotations missing required field", 331 }, 332 { 333 name: "missing type", 334 annotations: map[string]string{ 335 RootfsPrefix + "source": imagePath, 336 RootfsPrefix + "overlay": config.MemoryOverlay.String(), 337 }, 338 error: "rootfs annotations missing required field", 339 }, 340 } { 341 t.Run(tst.name, func(t *testing.T) { 342 spec := &specs.Spec{Annotations: tst.annotations} 343 hint, err := NewRootfsHint(spec) 344 if err == nil || !strings.Contains(err.Error(), tst.error) { 345 t.Errorf("NewRootfsHint invalid error, want: .*%s.*, got: %v", tst.error, err) 346 } 347 if hint != nil { 348 t.Errorf("NewRootfsHint must return nil on failure: %+v", hint) 349 } 350 }) 351 } 352 }