github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/cmd/snap-update-ns/update_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 26 . "gopkg.in/check.v1" 27 28 update "github.com/snapcore/snapd/cmd/snap-update-ns" 29 "github.com/snapcore/snapd/logger" 30 "github.com/snapcore/snapd/osutil" 31 "github.com/snapcore/snapd/testutil" 32 ) 33 34 type updateSuite struct { 35 testutil.BaseTest 36 log *bytes.Buffer 37 } 38 39 var _ = Suite(&updateSuite{}) 40 41 func (s *updateSuite) SetUpTest(c *C) { 42 s.BaseTest.SetUpTest(c) 43 buf, restore := logger.MockLogger() 44 s.BaseTest.AddCleanup(restore) 45 s.log = buf 46 } 47 48 func (s *updateSuite) TestSmoke(c *C) { 49 upCtx := &testProfileUpdateContext{} 50 c.Assert(update.ExecuteMountProfileUpdate(upCtx), IsNil) 51 } 52 53 func (s *updateSuite) TestUpdateFlow(c *C) { 54 // The flow of update is as follows: 55 // - the current profile and the desired profiles are loaded 56 // - the needed changes are computed 57 // - the needed changes are performed (one by one) 58 // - the updated current profile is saved 59 var funcsCalled []string 60 var nChanges int 61 upCtx := &testProfileUpdateContext{ 62 loadCurrentProfile: func() (*osutil.MountProfile, error) { 63 funcsCalled = append(funcsCalled, "loaded-current") 64 return &osutil.MountProfile{}, nil 65 }, 66 loadDesiredProfile: func() (*osutil.MountProfile, error) { 67 funcsCalled = append(funcsCalled, "loaded-desired") 68 return &osutil.MountProfile{}, nil 69 }, 70 neededChanges: func(old, new *osutil.MountProfile) []*update.Change { 71 funcsCalled = append(funcsCalled, "changes-computed") 72 return []*update.Change{{}, {}} 73 }, 74 performChange: func(change *update.Change, as *update.Assumptions) ([]*update.Change, error) { 75 nChanges++ 76 funcsCalled = append(funcsCalled, fmt.Sprintf("change-%d-performed", nChanges)) 77 return nil, nil 78 }, 79 saveCurrentProfile: func(*osutil.MountProfile) error { 80 funcsCalled = append(funcsCalled, "saved-current") 81 return nil 82 }, 83 } 84 restore := upCtx.MockRelatedFunctions() 85 defer restore() 86 c.Assert(update.ExecuteMountProfileUpdate(upCtx), IsNil) 87 c.Assert(funcsCalled, DeepEquals, []string{"loaded-desired", "loaded-current", 88 "changes-computed", "change-1-performed", "change-2-performed", "saved-current"}) 89 c.Assert(update.ExecuteMountProfileUpdate(upCtx), IsNil) 90 } 91 92 func (s *updateSuite) TestResultingProfile(c *C) { 93 // When the mount namespace is changed by performing actions the updated 94 // current profile is comprised of the past changes that were reused (kept 95 // unchanged) as well as newly mounted entries. Unmounted entries simple 96 // cease to be. 97 var saved *osutil.MountProfile 98 upCtx := &testProfileUpdateContext{ 99 neededChanges: func(old, new *osutil.MountProfile) []*update.Change { 100 return []*update.Change{ 101 {Action: update.Keep, Entry: osutil.MountEntry{Dir: "/keep"}}, 102 {Action: update.Unmount, Entry: osutil.MountEntry{Dir: "/unmount"}}, 103 {Action: update.Mount, Entry: osutil.MountEntry{Dir: "/mount"}}, 104 } 105 }, 106 saveCurrentProfile: func(profile *osutil.MountProfile) error { 107 saved = profile 108 return nil 109 }, 110 } 111 restore := upCtx.MockRelatedFunctions() 112 defer restore() 113 c.Assert(update.ExecuteMountProfileUpdate(upCtx), IsNil) 114 c.Check(saved, DeepEquals, &osutil.MountProfile{Entries: []osutil.MountEntry{ 115 {Dir: "/keep"}, 116 {Dir: "/mount"}, 117 }}) 118 } 119 120 func (s *updateSuite) TestSynthesizedPastChanges(c *C) { 121 // When an mount update is performed it runs under the assumption 122 // that past changes (i.e. the current profile) did occur. This is used 123 // by the trespassing detector. 124 text := `tmpfs /usr tmpfs 0 0` 125 entry, err := osutil.ParseMountEntry(text) 126 c.Assert(err, IsNil) 127 as := &update.Assumptions{} 128 upCtx := &testProfileUpdateContext{ 129 loadCurrentProfile: func() (*osutil.MountProfile, error) { return osutil.LoadMountProfileText(text) }, 130 loadDesiredProfile: func() (*osutil.MountProfile, error) { return osutil.LoadMountProfileText(text) }, 131 assumptions: func() *update.Assumptions { return as }, 132 } 133 restore := upCtx.MockRelatedFunctions() 134 defer restore() 135 136 // Perform the update, this will modify assumptions. 137 c.Check(as.PastChanges(), HasLen, 0) 138 c.Assert(update.ExecuteMountProfileUpdate(upCtx), IsNil) 139 c.Check(as.PastChanges(), HasLen, 1) 140 c.Check(as.PastChanges(), DeepEquals, []*update.Change{{ 141 Action: update.Mount, 142 Entry: entry, 143 }}) 144 } 145 146 func (s *updateSuite) TestSyntheticChanges(c *C) { 147 // When a mount change is performed it may cause additional mount changes 148 // to be performed, that were needed internally. Such changes are recorded 149 // and saved into the current profile. 150 var saved *osutil.MountProfile 151 upCtx := &testProfileUpdateContext{ 152 loadDesiredProfile: func() (*osutil.MountProfile, error) { 153 return &osutil.MountProfile{Entries: []osutil.MountEntry{ 154 {Dir: "/subdir/mount"}, 155 }}, nil 156 }, 157 saveCurrentProfile: func(profile *osutil.MountProfile) error { 158 saved = profile 159 return nil 160 }, 161 neededChanges: func(old, new *osutil.MountProfile) []*update.Change { 162 return []*update.Change{ 163 {Action: update.Mount, Entry: osutil.MountEntry{Dir: "/subdir/mount"}}, 164 } 165 }, 166 performChange: func(change *update.Change, as *update.Assumptions) ([]*update.Change, error) { 167 // If we are trying to mount /subdir/mount then synthesize a change 168 // for making a tmpfs on /subdir. 169 if change.Action == update.Mount && change.Entry.Dir == "/subdir/mount" { 170 return []*update.Change{ 171 {Action: update.Mount, Entry: osutil.MountEntry{Dir: "/subdir", Type: "tmpfs"}}, 172 }, nil 173 } 174 return nil, nil 175 }, 176 } 177 restore := upCtx.MockRelatedFunctions() 178 defer restore() 179 c.Assert(update.ExecuteMountProfileUpdate(upCtx), IsNil) 180 c.Check(saved, DeepEquals, &osutil.MountProfile{Entries: []osutil.MountEntry{ 181 {Dir: "/subdir", Type: "tmpfs"}, 182 {Dir: "/subdir/mount"}, 183 }}) 184 } 185 186 func (s *updateSuite) TestCannotPerformContentInterfaceChange(c *C) { 187 // When performing a mount change for a content interface fails, we simply 188 // ignore the error carry on. Such changes are not stored in the updated 189 // current profile. 190 var saved *osutil.MountProfile 191 upCtx := &testProfileUpdateContext{ 192 saveCurrentProfile: func(profile *osutil.MountProfile) error { 193 saved = profile 194 return nil 195 }, 196 neededChanges: func(old, new *osutil.MountProfile) []*update.Change { 197 return []*update.Change{ 198 {Action: update.Mount, Entry: osutil.MountEntry{Dir: "/dir-1"}}, 199 {Action: update.Mount, Entry: osutil.MountEntry{Dir: "/dir-2"}}, 200 {Action: update.Mount, Entry: osutil.MountEntry{Dir: "/dir-3"}}, 201 {Action: update.Mount, Entry: osutil.MountEntry{Dir: "/dir-4"}}, 202 } 203 }, 204 performChange: func(change *update.Change, as *update.Assumptions) ([]*update.Change, error) { 205 // The change to /dir-2 cannot be made. 206 if change.Action == update.Mount && change.Entry.Dir == "/dir-2" { 207 return nil, errTesting 208 } 209 // The change to /dir-4 cannot be made either but with a special reason. 210 if change.Action == update.Mount && change.Entry.Dir == "/dir-4" { 211 return nil, update.ErrIgnoredMissingMount 212 } 213 return nil, nil 214 }, 215 } 216 restore := upCtx.MockRelatedFunctions() 217 defer restore() 218 c.Assert(update.ExecuteMountProfileUpdate(upCtx), IsNil) 219 c.Check(saved, DeepEquals, &osutil.MountProfile{Entries: []osutil.MountEntry{ 220 {Dir: "/dir-1"}, 221 {Dir: "/dir-3"}, 222 }}) 223 // A message is logged though, unless specifically silenced with a crafted error. 224 c.Check(s.log.String(), testutil.Contains, "cannot change mount namespace according to change mount (none /dir-2 none defaults 0 0): testing") 225 c.Check(s.log.String(), Not(testutil.Contains), "cannot change mount namespace according to change mount (none /dir-4 none defaults 0 0): ") 226 } 227 228 func (s *updateSuite) TestCannotPerformLayoutChange(c *C) { 229 // When performing a mount change for a layout, errors are immediately fatal. 230 var saved *osutil.MountProfile 231 upCtx := &testProfileUpdateContext{ 232 saveCurrentProfile: func(profile *osutil.MountProfile) error { 233 saved = profile 234 return nil 235 }, 236 neededChanges: func(old, new *osutil.MountProfile) []*update.Change { 237 return []*update.Change{ 238 {Action: update.Mount, Entry: osutil.MountEntry{Dir: "/dir-1"}}, 239 {Action: update.Mount, Entry: osutil.MountEntry{Dir: "/dir-2", Options: []string{"x-snapd.origin=layout"}}}, 240 {Action: update.Mount, Entry: osutil.MountEntry{Dir: "/dir-3"}}, 241 } 242 }, 243 performChange: func(change *update.Change, as *update.Assumptions) ([]*update.Change, error) { 244 // The change to /dir-2 cannot be made. 245 if change.Action == update.Mount && change.Entry.Dir == "/dir-2" { 246 return nil, errTesting 247 } 248 return nil, nil 249 }, 250 } 251 restore := upCtx.MockRelatedFunctions() 252 defer restore() 253 err := update.ExecuteMountProfileUpdate(upCtx) 254 c.Check(err, Equals, errTesting) 255 c.Check(saved, IsNil) 256 } 257 258 func (s *updateSuite) TestCannotPerformOvermountChange(c *C) { 259 // When performing a mount change for an "overname", errors are immediately fatal. 260 var saved *osutil.MountProfile 261 upCtx := &testProfileUpdateContext{ 262 saveCurrentProfile: func(profile *osutil.MountProfile) error { 263 saved = profile 264 return nil 265 }, 266 neededChanges: func(old, new *osutil.MountProfile) []*update.Change { 267 return []*update.Change{ 268 {Action: update.Mount, Entry: osutil.MountEntry{Dir: "/dir-1"}}, 269 {Action: update.Mount, Entry: osutil.MountEntry{Dir: "/dir-2", Options: []string{"x-snapd.origin=overname"}}}, 270 {Action: update.Mount, Entry: osutil.MountEntry{Dir: "/dir-3"}}, 271 } 272 }, 273 performChange: func(change *update.Change, as *update.Assumptions) ([]*update.Change, error) { 274 // The change to /dir-2 cannot be made. 275 if change.Action == update.Mount && change.Entry.Dir == "/dir-2" { 276 return nil, errTesting 277 } 278 return nil, nil 279 }, 280 } 281 restore := upCtx.MockRelatedFunctions() 282 defer restore() 283 err := update.ExecuteMountProfileUpdate(upCtx) 284 c.Check(err, Equals, errTesting) 285 c.Check(saved, IsNil) 286 } 287 288 // testProfileUpdateContext implements MountProfileUpdateContext and is suitable for testing. 289 type testProfileUpdateContext struct { 290 loadCurrentProfile func() (*osutil.MountProfile, error) 291 loadDesiredProfile func() (*osutil.MountProfile, error) 292 saveCurrentProfile func(*osutil.MountProfile) error 293 assumptions func() *update.Assumptions 294 295 // The remaining functions are defined for consistency but are installed by 296 // calling their mock helpers. They are not a part of the interface. 297 neededChanges func(*osutil.MountProfile, *osutil.MountProfile) []*update.Change 298 performChange func(*update.Change, *update.Assumptions) ([]*update.Change, error) 299 } 300 301 // MockRelatedFunctions mocks NeededChanges and Change.Perform for the purpose of testing. 302 func (upCtx *testProfileUpdateContext) MockRelatedFunctions() (restore func()) { 303 neededChanges := func(*osutil.MountProfile, *osutil.MountProfile) []*update.Change { return nil } 304 if upCtx.neededChanges != nil { 305 neededChanges = upCtx.neededChanges 306 } 307 restore1 := update.MockNeededChanges(neededChanges) 308 309 performChange := func(*update.Change, *update.Assumptions) ([]*update.Change, error) { return nil, nil } 310 if upCtx.performChange != nil { 311 performChange = upCtx.performChange 312 } 313 restore2 := update.MockChangePerform(performChange) 314 315 return func() { 316 restore1() 317 restore2() 318 } 319 } 320 321 func (upCtx *testProfileUpdateContext) Lock() (unlock func(), err error) { 322 return func() {}, nil 323 } 324 325 func (upCtx *testProfileUpdateContext) Assumptions() *update.Assumptions { 326 if upCtx.assumptions != nil { 327 return upCtx.assumptions() 328 } 329 return &update.Assumptions{} 330 } 331 332 func (upCtx *testProfileUpdateContext) LoadCurrentProfile() (*osutil.MountProfile, error) { 333 if upCtx.loadCurrentProfile != nil { 334 return upCtx.loadCurrentProfile() 335 } 336 return &osutil.MountProfile{}, nil 337 } 338 339 func (upCtx *testProfileUpdateContext) LoadDesiredProfile() (*osutil.MountProfile, error) { 340 if upCtx.loadDesiredProfile != nil { 341 return upCtx.loadDesiredProfile() 342 } 343 return &osutil.MountProfile{}, nil 344 } 345 346 func (upCtx *testProfileUpdateContext) SaveCurrentProfile(profile *osutil.MountProfile) error { 347 if upCtx.saveCurrentProfile != nil { 348 return upCtx.saveCurrentProfile(profile) 349 } 350 return nil 351 }