github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/cmd/snap-update-ns/main_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2017 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package main_test 21 22 import ( 23 "bytes" 24 "fmt" 25 "io/ioutil" 26 "os" 27 "path/filepath" 28 "testing" 29 30 . "gopkg.in/check.v1" 31 32 update "github.com/snapcore/snapd/cmd/snap-update-ns" 33 "github.com/snapcore/snapd/dirs" 34 "github.com/snapcore/snapd/features" 35 "github.com/snapcore/snapd/logger" 36 "github.com/snapcore/snapd/osutil" 37 "github.com/snapcore/snapd/sandbox/cgroup" 38 "github.com/snapcore/snapd/testutil" 39 ) 40 41 func Test(t *testing.T) { TestingT(t) } 42 43 type mainSuite struct { 44 testutil.BaseTest 45 as *update.Assumptions 46 log *bytes.Buffer 47 } 48 49 var _ = Suite(&mainSuite{}) 50 51 func (s *mainSuite) SetUpTest(c *C) { 52 s.BaseTest.SetUpTest(c) 53 s.as = &update.Assumptions{} 54 buf, restore := logger.MockLogger() 55 s.AddCleanup(restore) 56 s.log = buf 57 s.AddCleanup(cgroup.MockVersion(cgroup.V1, nil)) 58 } 59 60 func (s *mainSuite) TestExecuteMountProfileUpdate(c *C) { 61 dirs.SetRootDir(c.MkDir()) 62 defer dirs.SetRootDir("/") 63 64 restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { 65 return nil, nil 66 }) 67 defer restore() 68 69 snapName := "foo" 70 desiredProfileContent := `/var/lib/snapd/hostfs/usr/share/fonts /usr/share/fonts none bind,ro 0 0 71 /var/lib/snapd/hostfs/usr/local/share/fonts /usr/local/share/fonts none bind,ro 0 0` 72 73 desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) 74 err := os.MkdirAll(filepath.Dir(desiredProfilePath), 0755) 75 c.Assert(err, IsNil) 76 err = ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644) 77 c.Assert(err, IsNil) 78 79 currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) 80 err = os.MkdirAll(filepath.Dir(currentProfilePath), 0755) 81 c.Assert(err, IsNil) 82 err = ioutil.WriteFile(currentProfilePath, nil, 0644) 83 c.Assert(err, IsNil) 84 85 upCtx := update.NewSystemProfileUpdateContext(snapName, false) 86 err = update.ExecuteMountProfileUpdate(upCtx) 87 c.Assert(err, IsNil) 88 89 c.Check(currentProfilePath, testutil.FileEquals, `/var/lib/snapd/hostfs/usr/local/share/fonts /usr/local/share/fonts none bind,ro 0 0 90 /var/lib/snapd/hostfs/usr/share/fonts /usr/share/fonts none bind,ro 0 0 91 `) 92 } 93 94 func (s *mainSuite) TestAddingSyntheticChanges(c *C) { 95 dirs.SetRootDir(c.MkDir()) 96 defer dirs.SetRootDir("/") 97 98 // The snap `mysnap` wishes to export it's usr/share/mysnap directory and 99 // make it appear as if it was in /usr/share/mysnap directly. 100 const snapName = "mysnap" 101 const currentProfileContent = "" 102 const desiredProfileContent = "/snap/mysnap/42/usr/share/mysnap /usr/share/mysnap none bind,ro 0 0" 103 104 currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) 105 desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) 106 107 c.Assert(os.MkdirAll(filepath.Dir(currentProfilePath), 0755), IsNil) 108 c.Assert(os.MkdirAll(filepath.Dir(desiredProfilePath), 0755), IsNil) 109 c.Assert(ioutil.WriteFile(currentProfilePath, []byte(currentProfileContent), 0644), IsNil) 110 c.Assert(ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644), IsNil) 111 112 // In order to make that work, /usr/share had to be converted to a writable 113 // mimic. Some actions were performed under the hood and now we see a 114 // subset of them as synthetic changes here. 115 // 116 // Note that if you compare this to the code that plans a writable mimic 117 // you will see that there are additional changes that are _not_ 118 // represented here. The changes have only one goal: tell 119 // snap-update-ns how the mimic can be undone in case it is no longer 120 // needed. 121 restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { 122 // The change that we were asked to perform is to create a bind mount 123 // from within the snap to /usr/share/mysnap. 124 c.Assert(chg, DeepEquals, &update.Change{ 125 Action: update.Mount, Entry: osutil.MountEntry{ 126 Name: "/snap/mysnap/42/usr/share/mysnap", 127 Dir: "/usr/share/mysnap", Type: "none", 128 Options: []string{"bind", "ro"}}}) 129 synthetic := []*update.Change{ 130 // The original directory (which was a part of the core snap and is 131 // read only) was hidden with a tmpfs. 132 {Action: update.Mount, Entry: osutil.MountEntry{ 133 Dir: "/usr/share", Name: "tmpfs", Type: "tmpfs", 134 Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap"}}}, 135 // For the sake of brevity we will only represent a few of the 136 // entries typically there. Normally this list can get quite long. 137 // Also note that the entry is a little fake. In reality it was 138 // constructed using a temporary bind mount that contained the 139 // original mount entries of /usr/share but this fact was lost. 140 // Again, the only point of this entry is to correctly perform an 141 // undo operation when /usr/share/mysnap is no longer needed. 142 {Action: update.Mount, Entry: osutil.MountEntry{ 143 Dir: "/usr/share/adduser", Name: "/usr/share/adduser", 144 Options: []string{"bind", "ro", "x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap"}}}, 145 {Action: update.Mount, Entry: osutil.MountEntry{ 146 Dir: "/usr/share/awk", Name: "/usr/share/awk", 147 Options: []string{"bind", "ro", "x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap"}}}, 148 } 149 return synthetic, nil 150 }) 151 defer restore() 152 153 upCtx := update.NewSystemProfileUpdateContext(snapName, false) 154 c.Assert(update.ExecuteMountProfileUpdate(upCtx), IsNil) 155 156 c.Check(currentProfilePath, testutil.FileEquals, 157 `tmpfs /usr/share tmpfs x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 158 /usr/share/adduser /usr/share/adduser none bind,ro,x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 159 /usr/share/awk /usr/share/awk none bind,ro,x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 160 /snap/mysnap/42/usr/share/mysnap /usr/share/mysnap none bind,ro 0 0 161 `) 162 } 163 164 func (s *mainSuite) TestRemovingSyntheticChanges(c *C) { 165 dirs.SetRootDir(c.MkDir()) 166 defer dirs.SetRootDir("/") 167 168 c.Assert(os.MkdirAll(dirs.FeaturesDir, 0755), IsNil) 169 c.Assert(ioutil.WriteFile(features.RobustMountNamespaceUpdates.ControlFile(), []byte(nil), 0644), IsNil) 170 171 // The snap `mysnap` no longer wishes to export it's usr/share/mysnap 172 // directory. All the synthetic changes that were associated with that mount 173 // entry can be discarded. 174 const snapName = "mysnap" 175 const currentProfileContent = `tmpfs /usr/share tmpfs x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 176 /usr/share/adduser /usr/share/adduser none bind,ro,x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 177 /usr/share/awk /usr/share/awk none bind,ro,x-snapd.synthetic,x-snapd.needed-by=/usr/share/mysnap 0 0 178 /snap/mysnap/42/usr/share/mysnap /usr/share/mysnap none bind,ro 0 0 179 ` 180 const desiredProfileContent = "" 181 182 currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) 183 desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) 184 185 c.Assert(os.MkdirAll(filepath.Dir(currentProfilePath), 0755), IsNil) 186 c.Assert(os.MkdirAll(filepath.Dir(desiredProfilePath), 0755), IsNil) 187 c.Assert(ioutil.WriteFile(currentProfilePath, []byte(currentProfileContent), 0644), IsNil) 188 c.Assert(ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644), IsNil) 189 190 n := -1 191 restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { 192 n++ 193 switch n { 194 case 0: 195 c.Assert(chg, DeepEquals, &update.Change{ 196 Action: update.Unmount, 197 Entry: osutil.MountEntry{ 198 Name: "/snap/mysnap/42/usr/share/mysnap", 199 Dir: "/usr/share/mysnap", Type: "none", 200 Options: []string{"bind", "ro"}, 201 }, 202 }) 203 case 1: 204 c.Assert(chg, DeepEquals, &update.Change{ 205 Action: update.Unmount, 206 Entry: osutil.MountEntry{ 207 Name: "tmpfs", Dir: "/usr/share", Type: "tmpfs", 208 Options: []string{"x-snapd.synthetic", "x-snapd.needed-by=/usr/share/mysnap", "x-snapd.detach"}, 209 }, 210 }) 211 default: 212 panic(fmt.Sprintf("unexpected call n=%d, chg: %v", n, *chg)) 213 } 214 return nil, nil 215 }) 216 defer restore() 217 218 upCtx := update.NewSystemProfileUpdateContext(snapName, false) 219 c.Assert(update.ExecuteMountProfileUpdate(upCtx), IsNil) 220 221 c.Check(currentProfilePath, testutil.FileEquals, "") 222 } 223 224 func (s *mainSuite) TestApplyingLayoutChanges(c *C) { 225 dirs.SetRootDir(c.MkDir()) 226 defer dirs.SetRootDir("/") 227 228 const snapName = "mysnap" 229 const currentProfileContent = "" 230 const desiredProfileContent = "/snap/mysnap/42/usr/share/mysnap /usr/share/mysnap none bind,ro,x-snapd.origin=layout 0 0" 231 232 currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) 233 desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) 234 235 c.Assert(os.MkdirAll(filepath.Dir(currentProfilePath), 0755), IsNil) 236 c.Assert(os.MkdirAll(filepath.Dir(desiredProfilePath), 0755), IsNil) 237 c.Assert(ioutil.WriteFile(currentProfilePath, []byte(currentProfileContent), 0644), IsNil) 238 c.Assert(ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644), IsNil) 239 240 n := -1 241 restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { 242 n++ 243 switch n { 244 case 0: 245 c.Assert(chg, DeepEquals, &update.Change{ 246 Action: update.Mount, 247 Entry: osutil.MountEntry{ 248 Name: "/snap/mysnap/42/usr/share/mysnap", 249 Dir: "/usr/share/mysnap", Type: "none", 250 Options: []string{"bind", "ro", "x-snapd.origin=layout"}, 251 }, 252 }) 253 return nil, fmt.Errorf("testing") 254 default: 255 panic(fmt.Sprintf("unexpected call n=%d, chg: %v", n, *chg)) 256 } 257 }) 258 defer restore() 259 260 // The error was not ignored, we bailed out. 261 upCtx := update.NewSystemProfileUpdateContext(snapName, false) 262 c.Assert(update.ExecuteMountProfileUpdate(upCtx), ErrorMatches, "testing") 263 264 c.Check(currentProfilePath, testutil.FileEquals, "") 265 } 266 267 func (s *mainSuite) TestApplyingParallelInstanceChanges(c *C) { 268 dirs.SetRootDir(c.MkDir()) 269 defer dirs.SetRootDir("/") 270 271 const snapName = "mysnap" 272 const currentProfileContent = "" 273 const desiredProfileContent = "/snap/mysnap_foo /snap/mysnap none rbind,x-snapd.origin=overname 0 0" 274 275 currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) 276 desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) 277 278 c.Assert(os.MkdirAll(filepath.Dir(currentProfilePath), 0755), IsNil) 279 c.Assert(os.MkdirAll(filepath.Dir(desiredProfilePath), 0755), IsNil) 280 c.Assert(ioutil.WriteFile(currentProfilePath, []byte(currentProfileContent), 0644), IsNil) 281 c.Assert(ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644), IsNil) 282 283 n := -1 284 restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { 285 n++ 286 switch n { 287 case 0: 288 c.Assert(chg, DeepEquals, &update.Change{ 289 Action: update.Mount, 290 Entry: osutil.MountEntry{ 291 Name: "/snap/mysnap_foo", 292 Dir: "/snap/mysnap", Type: "none", 293 Options: []string{"rbind", "x-snapd.origin=overname"}, 294 }, 295 }) 296 return nil, fmt.Errorf("testing") 297 default: 298 panic(fmt.Sprintf("unexpected call n=%d, chg: %v", n, *chg)) 299 } 300 }) 301 defer restore() 302 303 // The error was not ignored, we bailed out. 304 upCtx := update.NewSystemProfileUpdateContext(snapName, false) 305 c.Assert(update.ExecuteMountProfileUpdate(upCtx), ErrorMatches, "testing") 306 307 c.Check(currentProfilePath, testutil.FileEquals, "") 308 } 309 310 func (s *mainSuite) TestApplyIgnoredMissingMount(c *C) { 311 dirs.SetRootDir(c.MkDir()) 312 defer dirs.SetRootDir("/") 313 314 const snapName = "mysnap" 315 const currentProfileContent = "" 316 const desiredProfileContent = "/source /target none bind,x-snapd.ignore-missing 0 0" 317 318 currentProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapRunNsDir, snapName) 319 desiredProfilePath := fmt.Sprintf("%s/snap.%s.fstab", dirs.SnapMountPolicyDir, snapName) 320 321 c.Assert(os.MkdirAll(filepath.Dir(currentProfilePath), 0755), IsNil) 322 c.Assert(os.MkdirAll(filepath.Dir(desiredProfilePath), 0755), IsNil) 323 c.Assert(ioutil.WriteFile(currentProfilePath, []byte(currentProfileContent), 0644), IsNil) 324 c.Assert(ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644), IsNil) 325 326 n := -1 327 restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { 328 n++ 329 switch n { 330 case 0: 331 c.Assert(chg, DeepEquals, &update.Change{ 332 Action: update.Mount, 333 Entry: osutil.MountEntry{ 334 Name: "/source", 335 Dir: "/target", 336 Type: "none", 337 Options: []string{"bind", "x-snapd.ignore-missing"}, 338 }, 339 }) 340 return nil, update.ErrIgnoredMissingMount 341 default: 342 panic(fmt.Sprintf("unexpected call n=%d, chg: %v", n, *chg)) 343 } 344 }) 345 defer restore() 346 347 // The error was ignored, and no mount was recorded in the profile 348 upCtx := update.NewSystemProfileUpdateContext(snapName, false) 349 c.Assert(update.ExecuteMountProfileUpdate(upCtx), IsNil) 350 c.Check(s.log.String(), Equals, "") 351 c.Check(currentProfilePath, testutil.FileEquals, "") 352 } 353 354 func (s *mainSuite) TestApplyUserFstab(c *C) { 355 dirs.SetRootDir(c.MkDir()) 356 defer dirs.SetRootDir("/") 357 358 var changes []update.Change 359 restore := update.MockChangePerform(func(chg *update.Change, as *update.Assumptions) ([]*update.Change, error) { 360 changes = append(changes, *chg) 361 return nil, nil 362 }) 363 defer restore() 364 365 snapName := "foo" 366 desiredProfileContent := `$XDG_RUNTIME_DIR/doc/by-app/snap.foo $XDG_RUNTIME_DIR/doc none bind,rw 0 0` 367 368 desiredProfilePath := fmt.Sprintf("%s/snap.%s.user-fstab", dirs.SnapMountPolicyDir, snapName) 369 err := os.MkdirAll(filepath.Dir(desiredProfilePath), 0755) 370 c.Assert(err, IsNil) 371 err = ioutil.WriteFile(desiredProfilePath, []byte(desiredProfileContent), 0644) 372 c.Assert(err, IsNil) 373 374 upCtx := update.NewUserProfileUpdateContext(snapName, true, 1000) 375 err = update.ExecuteMountProfileUpdate(upCtx) 376 c.Assert(err, IsNil) 377 378 xdgRuntimeDir := fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, 1000) 379 380 c.Assert(changes, HasLen, 1) 381 c.Assert(changes[0].Action, Equals, update.Mount) 382 c.Assert(changes[0].Entry.Name, Equals, xdgRuntimeDir+"/doc/by-app/snap.foo") 383 c.Assert(changes[0].Entry.Dir, Matches, xdgRuntimeDir+"/doc") 384 }