github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/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  }