github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/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  }