github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/gadget/mountedfilesystem_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019 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 gadget_test
    21  
    22  import (
    23  	"errors"
    24  	"fmt"
    25  	"os"
    26  	"path/filepath"
    27  	"strings"
    28  
    29  	. "gopkg.in/check.v1"
    30  
    31  	"github.com/snapcore/snapd/gadget"
    32  	"github.com/snapcore/snapd/osutil"
    33  	"github.com/snapcore/snapd/strutil"
    34  	"github.com/snapcore/snapd/testutil"
    35  )
    36  
    37  type mountedfilesystemTestSuite struct {
    38  	dir    string
    39  	backup string
    40  }
    41  
    42  var _ = Suite(&mountedfilesystemTestSuite{})
    43  
    44  func (s *mountedfilesystemTestSuite) SetUpTest(c *C) {
    45  	s.dir = c.MkDir()
    46  	s.backup = c.MkDir()
    47  }
    48  
    49  type gadgetData struct {
    50  	name, target, symlinkTo, content string
    51  }
    52  
    53  func makeGadgetData(c *C, where string, data []gadgetData) {
    54  	for _, en := range data {
    55  		if en.name == "" {
    56  			continue
    57  		}
    58  		if strings.HasSuffix(en.name, "/") {
    59  			err := os.MkdirAll(filepath.Join(where, en.name), 0755)
    60  			c.Check(en.content, HasLen, 0)
    61  			c.Assert(err, IsNil)
    62  			continue
    63  		}
    64  		if en.symlinkTo != "" {
    65  			err := os.Symlink(en.symlinkTo, filepath.Join(where, en.name))
    66  			c.Assert(err, IsNil)
    67  			continue
    68  		}
    69  		makeSizedFile(c, filepath.Join(where, en.name), 0, []byte(en.content))
    70  	}
    71  }
    72  
    73  func verifyWrittenGadgetData(c *C, where string, data []gadgetData) {
    74  	for _, en := range data {
    75  		if en.target == "" {
    76  			continue
    77  		}
    78  		if en.symlinkTo != "" {
    79  			symlinkTarget, err := os.Readlink(filepath.Join(where, en.target))
    80  			c.Assert(err, IsNil)
    81  			c.Check(symlinkTarget, Equals, en.symlinkTo)
    82  			continue
    83  		}
    84  		target := filepath.Join(where, en.target)
    85  		c.Check(target, testutil.FileContains, en.content)
    86  	}
    87  }
    88  
    89  func makeExistingData(c *C, where string, data []gadgetData) {
    90  	for _, en := range data {
    91  		if en.target == "" {
    92  			continue
    93  		}
    94  		if strings.HasSuffix(en.target, "/") {
    95  			err := os.MkdirAll(filepath.Join(where, en.target), 0755)
    96  			c.Check(en.content, HasLen, 0)
    97  			c.Assert(err, IsNil)
    98  			continue
    99  		}
   100  		if en.symlinkTo != "" {
   101  			err := os.Symlink(en.symlinkTo, filepath.Join(where, en.target))
   102  			c.Assert(err, IsNil)
   103  			continue
   104  		}
   105  		makeSizedFile(c, filepath.Join(where, en.target), 0, []byte(en.content))
   106  	}
   107  }
   108  
   109  type contentType int
   110  
   111  const (
   112  	typeFile contentType = iota
   113  	typeDir
   114  )
   115  
   116  func verifyDirContents(c *C, where string, expected map[string]contentType) {
   117  	cleanWhere := filepath.Clean(where)
   118  
   119  	got := make(map[string]contentType)
   120  	err := filepath.Walk(where, func(name string, fi os.FileInfo, err error) error {
   121  		if err != nil {
   122  			return err
   123  		}
   124  		if name == where {
   125  			return nil
   126  		}
   127  		suffixName := name[len(cleanWhere)+1:]
   128  		t := typeFile
   129  		if fi.IsDir() {
   130  			t = typeDir
   131  		}
   132  		got[suffixName] = t
   133  
   134  		for prefix := filepath.Dir(name); prefix != where; prefix = filepath.Dir(prefix) {
   135  			delete(got, prefix[len(cleanWhere)+1:])
   136  		}
   137  
   138  		return nil
   139  	})
   140  	c.Assert(err, IsNil)
   141  	if len(expected) > 0 {
   142  		c.Assert(got, DeepEquals, expected)
   143  	} else {
   144  		c.Assert(got, HasLen, 0)
   145  	}
   146  }
   147  
   148  func (s *mountedfilesystemTestSuite) TestWriteFile(c *C) {
   149  	makeSizedFile(c, filepath.Join(s.dir, "foo"), 0, []byte("foo foo foo"))
   150  
   151  	outDir := c.MkDir()
   152  
   153  	// foo -> /foo
   154  	err := gadget.WriteFile(filepath.Join(s.dir, "foo"), filepath.Join(outDir, "foo"), nil)
   155  	c.Assert(err, IsNil)
   156  	c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, []byte("foo foo foo"))
   157  
   158  	// foo -> bar/foo
   159  	err = gadget.WriteFile(filepath.Join(s.dir, "foo"), filepath.Join(outDir, "bar/foo"), nil)
   160  	c.Assert(err, IsNil)
   161  	c.Check(filepath.Join(outDir, "bar/foo"), testutil.FileEquals, []byte("foo foo foo"))
   162  
   163  	// overwrites
   164  	makeSizedFile(c, filepath.Join(outDir, "overwrite"), 0, []byte("disappear"))
   165  	err = gadget.WriteFile(filepath.Join(s.dir, "foo"), filepath.Join(outDir, "overwrite"), nil)
   166  	c.Assert(err, IsNil)
   167  	c.Check(filepath.Join(outDir, "overwrite"), testutil.FileEquals, []byte("foo foo foo"))
   168  
   169  	// unless told to preserve
   170  	keepName := filepath.Join(outDir, "keep")
   171  	makeSizedFile(c, keepName, 0, []byte("can't touch this"))
   172  	err = gadget.WriteFile(filepath.Join(s.dir, "foo"), filepath.Join(outDir, "keep"), []string{keepName})
   173  	c.Assert(err, IsNil)
   174  	c.Check(filepath.Join(outDir, "keep"), testutil.FileEquals, []byte("can't touch this"))
   175  
   176  	err = gadget.WriteFile(filepath.Join(s.dir, "not-found"), filepath.Join(outDir, "foo"), nil)
   177  	c.Assert(err, ErrorMatches, "cannot copy .*: unable to open .*/not-found: .* no such file or directory")
   178  }
   179  
   180  func (s *mountedfilesystemTestSuite) TestWriteDirectoryContents(c *C) {
   181  	gd := []gadgetData{
   182  		{name: "boot-assets/splash", target: "splash", content: "splash"},
   183  		{name: "boot-assets/some-dir/data", target: "some-dir/data", content: "data"},
   184  		{name: "boot-assets/some-dir/empty-file", target: "some-dir/empty-file"},
   185  		{name: "boot-assets/nested-dir/nested", target: "/nested-dir/nested", content: "nested"},
   186  		{name: "boot-assets/nested-dir/more-nested/more", target: "/nested-dir/more-nested/more", content: "more"},
   187  	}
   188  	makeGadgetData(c, s.dir, gd)
   189  
   190  	ps := &gadget.LaidOutStructure{
   191  		VolumeStructure: &gadget.VolumeStructure{
   192  			Filesystem: "ext4",
   193  		},
   194  	}
   195  	rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil)
   196  	c.Assert(err, IsNil)
   197  
   198  	outDir := c.MkDir()
   199  	// boot-assets/ -> / (contents of boot assets under /)
   200  	err = rw.WriteDirectory(outDir, filepath.Join(s.dir, "boot-assets")+"/", outDir+"/", nil)
   201  	c.Assert(err, IsNil)
   202  
   203  	verifyWrittenGadgetData(c, outDir, gd)
   204  }
   205  
   206  func (s *mountedfilesystemTestSuite) TestWriteDirectoryWhole(c *C) {
   207  	gd := []gadgetData{
   208  		{name: "boot-assets/splash", target: "boot-assets/splash", content: "splash"},
   209  		{name: "boot-assets/some-dir/data", target: "boot-assets/some-dir/data", content: "data"},
   210  		{name: "boot-assets/some-dir/empty-file", target: "boot-assets/some-dir/empty-file"},
   211  		{name: "boot-assets/nested-dir/nested", target: "boot-assets/nested-dir/nested", content: "nested"},
   212  		{name: "boot-assets/nested-dir/more-nested/more", target: "boot-assets//nested-dir/more-nested/more", content: "more"},
   213  	}
   214  	makeGadgetData(c, s.dir, gd)
   215  
   216  	ps := &gadget.LaidOutStructure{
   217  		VolumeStructure: &gadget.VolumeStructure{
   218  			Filesystem: "ext4",
   219  		},
   220  	}
   221  	rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil)
   222  	c.Assert(err, IsNil)
   223  
   224  	outDir := c.MkDir()
   225  	// boot-assets -> / (boot-assets and children under /)
   226  	err = rw.WriteDirectory(outDir, filepath.Join(s.dir, "boot-assets"), outDir+"/", nil)
   227  	c.Assert(err, IsNil)
   228  
   229  	verifyWrittenGadgetData(c, outDir, gd)
   230  }
   231  
   232  func (s *mountedfilesystemTestSuite) TestWriteNonDirectory(c *C) {
   233  	gd := []gadgetData{
   234  		{name: "foo", content: "nested"},
   235  	}
   236  	makeGadgetData(c, s.dir, gd)
   237  	ps := &gadget.LaidOutStructure{
   238  		VolumeStructure: &gadget.VolumeStructure{
   239  			Filesystem: "ext4",
   240  		},
   241  	}
   242  	rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil)
   243  	c.Assert(err, IsNil)
   244  
   245  	outDir := c.MkDir()
   246  
   247  	err = rw.WriteDirectory(outDir, filepath.Join(s.dir, "foo")+"/", outDir, nil)
   248  	c.Assert(err, ErrorMatches, `cannot specify trailing / for a source which is not a directory`)
   249  
   250  	err = rw.WriteDirectory(outDir, filepath.Join(s.dir, "foo"), outDir, nil)
   251  	c.Assert(err, ErrorMatches, `source is not a directory`)
   252  }
   253  
   254  type mockContentChange struct {
   255  	path   string
   256  	change *gadget.ContentChange
   257  }
   258  
   259  type mockWriteObserver struct {
   260  	content         map[string][]*mockContentChange
   261  	preserveTargets []string
   262  	observeErr      error
   263  	expectedStruct  *gadget.LaidOutStructure
   264  	c               *C
   265  }
   266  
   267  func (m *mockWriteObserver) Observe(op gadget.ContentOperation, sourceStruct *gadget.LaidOutStructure,
   268  	targetRootDir, relativeTargetPath string, data *gadget.ContentChange) (gadget.ContentChangeAction, error) {
   269  	if m.c == nil {
   270  		panic("c is unset")
   271  	}
   272  	m.c.Assert(data, NotNil)
   273  	m.c.Assert(op, Equals, gadget.ContentWrite, Commentf("unexpected operation %v", op))
   274  	if m.content == nil {
   275  		m.content = make(map[string][]*mockContentChange)
   276  	}
   277  	// the file with content that will be written must exist
   278  	m.c.Check(osutil.FileExists(data.After) && !osutil.IsDirectory(data.After), Equals, true,
   279  		Commentf("path %q does not exist or is a directory", data.After))
   280  	// all files are treated as new by the writer
   281  	m.c.Check(data.Before, Equals, "")
   282  	m.c.Check(filepath.IsAbs(relativeTargetPath), Equals, false,
   283  		Commentf("target path %q is absolute", relativeTargetPath))
   284  
   285  	m.content[targetRootDir] = append(m.content[targetRootDir],
   286  		&mockContentChange{path: relativeTargetPath, change: data})
   287  
   288  	m.c.Assert(sourceStruct, NotNil)
   289  	m.c.Check(m.expectedStruct, DeepEquals, sourceStruct)
   290  
   291  	if strutil.ListContains(m.preserveTargets, relativeTargetPath) {
   292  		return gadget.ChangeIgnore, nil
   293  	}
   294  	return gadget.ChangeApply, m.observeErr
   295  }
   296  
   297  func (s *mountedfilesystemTestSuite) TestMountedWriterHappy(c *C) {
   298  	gd := []gadgetData{
   299  		{name: "foo", target: "foo-dir/foo", content: "foo foo foo"},
   300  		{name: "bar", target: "bar-name", content: "bar bar bar"},
   301  		{name: "boot-assets/splash", target: "splash", content: "splash"},
   302  		{name: "boot-assets/some-dir/data", target: "some-dir/data", content: "data"},
   303  		{name: "boot-assets/some-dir/data", target: "data-copy", content: "data"},
   304  		{name: "boot-assets/some-dir/empty-file", target: "some-dir/empty-file"},
   305  		{name: "boot-assets/nested-dir/nested", target: "/nested-copy/nested", content: "nested"},
   306  		{name: "boot-assets/nested-dir/more-nested/more", target: "/nested-copy/more-nested/more", content: "more"},
   307  		{name: "baz", target: "/baz", content: "baz"},
   308  	}
   309  	makeGadgetData(c, s.dir, gd)
   310  	err := os.MkdirAll(filepath.Join(s.dir, "boot-assets/empty-dir"), 0755)
   311  	c.Assert(err, IsNil)
   312  
   313  	ps := &gadget.LaidOutStructure{
   314  		VolumeStructure: &gadget.VolumeStructure{
   315  			Name:       "hello",
   316  			Size:       2048,
   317  			Filesystem: "ext4",
   318  			Content: []gadget.VolumeContent{
   319  				{
   320  					// single file in target directory
   321  					UnresolvedSource: "foo",
   322  					Target:           "/foo-dir/",
   323  				}, {
   324  					// single file under different name
   325  					UnresolvedSource: "bar",
   326  					Target:           "/bar-name",
   327  				}, {
   328  					// whole directory contents
   329  					UnresolvedSource: "boot-assets/",
   330  					Target:           "/",
   331  				}, {
   332  					// single file from nested directory
   333  					UnresolvedSource: "boot-assets/some-dir/data",
   334  					Target:           "/data-copy",
   335  				}, {
   336  					// contents of nested directory under new target directory
   337  					UnresolvedSource: "boot-assets/nested-dir/",
   338  					Target:           "/nested-copy/",
   339  				}, {
   340  					// contents of nested directory under new target directory
   341  					UnresolvedSource: "baz",
   342  					Target:           "baz",
   343  				},
   344  			},
   345  		},
   346  	}
   347  
   348  	outDir := c.MkDir()
   349  
   350  	obs := &mockWriteObserver{
   351  		c:              c,
   352  		expectedStruct: ps,
   353  	}
   354  	rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, obs)
   355  	c.Assert(err, IsNil)
   356  	c.Assert(rw, NotNil)
   357  
   358  	err = rw.Write(outDir, nil)
   359  	c.Assert(err, IsNil)
   360  
   361  	verifyWrittenGadgetData(c, outDir, gd)
   362  	c.Assert(osutil.IsDirectory(filepath.Join(outDir, "empty-dir")), Equals, true)
   363  
   364  	// verify observer was notified of writes for files only
   365  	c.Assert(obs.content, DeepEquals, map[string][]*mockContentChange{
   366  		outDir: {
   367  			{"foo-dir/foo", &gadget.ContentChange{After: filepath.Join(s.dir, "foo")}},
   368  			{"bar-name", &gadget.ContentChange{After: filepath.Join(s.dir, "bar")}},
   369  
   370  			{"nested-dir/more-nested/more", &gadget.ContentChange{
   371  				After: filepath.Join(s.dir, "boot-assets/nested-dir/more-nested/more"),
   372  			}},
   373  			{"nested-dir/nested", &gadget.ContentChange{
   374  				After: filepath.Join(s.dir, "boot-assets/nested-dir/nested"),
   375  			}},
   376  			{"some-dir/data", &gadget.ContentChange{
   377  				After: filepath.Join(s.dir, "boot-assets/some-dir/data"),
   378  			}},
   379  			{"some-dir/empty-file", &gadget.ContentChange{
   380  				After: filepath.Join(s.dir, "boot-assets/some-dir/empty-file"),
   381  			}},
   382  			{"splash", &gadget.ContentChange{
   383  				After: filepath.Join(s.dir, "boot-assets/splash"),
   384  			}},
   385  
   386  			{"data-copy", &gadget.ContentChange{
   387  				After: filepath.Join(s.dir, "boot-assets/some-dir/data"),
   388  			}},
   389  
   390  			{"nested-copy/more-nested/more", &gadget.ContentChange{
   391  				After: filepath.Join(s.dir, "boot-assets/nested-dir/more-nested/more"),
   392  			}},
   393  			{"nested-copy/nested", &gadget.ContentChange{
   394  				After: filepath.Join(s.dir, "boot-assets/nested-dir/nested"),
   395  			}},
   396  
   397  			{"baz", &gadget.ContentChange{After: filepath.Join(s.dir, "baz")}},
   398  		},
   399  	})
   400  }
   401  
   402  func (s *mountedfilesystemTestSuite) TestMountedWriterNonDirectory(c *C) {
   403  	gd := []gadgetData{
   404  		{name: "foo", content: "nested"},
   405  	}
   406  	makeGadgetData(c, s.dir, gd)
   407  
   408  	ps := &gadget.LaidOutStructure{
   409  		VolumeStructure: &gadget.VolumeStructure{
   410  			Size:       2048,
   411  			Filesystem: "ext4",
   412  			Content: []gadget.VolumeContent{
   413  				{
   414  					// contents of nested directory under new target directory
   415  					UnresolvedSource: "foo/",
   416  					Target:           "/nested-copy/",
   417  				},
   418  			},
   419  		},
   420  	}
   421  
   422  	outDir := c.MkDir()
   423  
   424  	rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil)
   425  	c.Assert(err, IsNil)
   426  	c.Assert(rw, NotNil)
   427  
   428  	err = rw.Write(outDir, nil)
   429  	c.Assert(err, ErrorMatches, `cannot write filesystem content of source:foo/: cannot specify trailing / for a source which is not a directory`)
   430  }
   431  
   432  func (s *mountedfilesystemTestSuite) TestMountedWriterErrorMissingSource(c *C) {
   433  	ps := &gadget.LaidOutStructure{
   434  		VolumeStructure: &gadget.VolumeStructure{
   435  			Size:       2048,
   436  			Filesystem: "ext4",
   437  			Content: []gadget.VolumeContent{
   438  				{
   439  					// single file in target directory
   440  					UnresolvedSource: "foo",
   441  					Target:           "/foo-dir/",
   442  				},
   443  			},
   444  		},
   445  	}
   446  
   447  	outDir := c.MkDir()
   448  
   449  	rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil)
   450  	c.Assert(err, IsNil)
   451  	c.Assert(rw, NotNil)
   452  
   453  	err = rw.Write(outDir, nil)
   454  	c.Assert(err, ErrorMatches, "cannot write filesystem content of source:foo: .*unable to open.*: no such file or directory")
   455  }
   456  
   457  func (s *mountedfilesystemTestSuite) TestMountedWriterErrorBadDestination(c *C) {
   458  	if os.Geteuid() == 0 {
   459  		c.Skip("the test cannot be run by the root user")
   460  	}
   461  
   462  	makeSizedFile(c, filepath.Join(s.dir, "foo"), 0, []byte("foo foo foo"))
   463  
   464  	ps := &gadget.LaidOutStructure{
   465  		VolumeStructure: &gadget.VolumeStructure{
   466  			Size:       2048,
   467  			Filesystem: "vfat",
   468  			Content: []gadget.VolumeContent{
   469  				{
   470  					// single file in target directory
   471  					UnresolvedSource: "foo",
   472  					Target:           "/foo-dir/",
   473  				},
   474  			},
   475  		},
   476  	}
   477  
   478  	outDir := c.MkDir()
   479  
   480  	err := os.Chmod(outDir, 0000)
   481  	c.Assert(err, IsNil)
   482  
   483  	rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil)
   484  	c.Assert(err, IsNil)
   485  	c.Assert(rw, NotNil)
   486  
   487  	err = rw.Write(outDir, nil)
   488  	c.Assert(err, ErrorMatches, "cannot write filesystem content of source:foo: cannot create .*: mkdir .* permission denied")
   489  }
   490  
   491  func (s *mountedfilesystemTestSuite) TestMountedWriterConflictingDestinationDirectoryErrors(c *C) {
   492  	makeGadgetData(c, s.dir, []gadgetData{
   493  		{name: "foo", content: "foo foo foo"},
   494  		{name: "foo-dir", content: "bar bar bar"},
   495  	})
   496  
   497  	psOverwritesDirectoryWithFile := &gadget.LaidOutStructure{
   498  		VolumeStructure: &gadget.VolumeStructure{
   499  			Size:       2048,
   500  			Filesystem: "ext4",
   501  			Content: []gadget.VolumeContent{
   502  				{
   503  					// single file in target directory
   504  					UnresolvedSource: "foo",
   505  					Target:           "/foo-dir/",
   506  				}, {
   507  					// conflicts with /foo-dir directory
   508  					UnresolvedSource: "foo-dir",
   509  					Target:           "/",
   510  				},
   511  			},
   512  		},
   513  	}
   514  
   515  	outDir := c.MkDir()
   516  
   517  	rw, err := gadget.NewMountedFilesystemWriter(s.dir, psOverwritesDirectoryWithFile, nil)
   518  	c.Assert(err, IsNil)
   519  	c.Assert(rw, NotNil)
   520  
   521  	// can't overwrite a directory with a file
   522  	err = rw.Write(outDir, nil)
   523  	c.Assert(err, ErrorMatches, fmt.Sprintf(`cannot write filesystem content of source:foo-dir: cannot copy .*: cannot commit atomic file copy: rename %[1]s/foo-dir\.[a-zA-Z0-9]+~ %[1]s/foo-dir: file exists`, outDir))
   524  
   525  }
   526  
   527  func (s *mountedfilesystemTestSuite) TestMountedWriterConflictingDestinationFileOk(c *C) {
   528  	makeGadgetData(c, s.dir, []gadgetData{
   529  		{name: "foo", content: "foo foo foo"},
   530  		{name: "bar", content: "bar bar bar"},
   531  	})
   532  	psOverwritesFile := &gadget.LaidOutStructure{
   533  		VolumeStructure: &gadget.VolumeStructure{
   534  			Size:       2048,
   535  			Filesystem: "ext4",
   536  			Content: []gadget.VolumeContent{
   537  				{
   538  					UnresolvedSource: "bar",
   539  					Target:           "/",
   540  				}, {
   541  					// overwrites data from preceding entry
   542  					UnresolvedSource: "foo",
   543  					Target:           "/bar",
   544  				},
   545  			},
   546  		},
   547  	}
   548  
   549  	outDir := c.MkDir()
   550  
   551  	rw, err := gadget.NewMountedFilesystemWriter(s.dir, psOverwritesFile, nil)
   552  	c.Assert(err, IsNil)
   553  	c.Assert(rw, NotNil)
   554  
   555  	err = rw.Write(outDir, nil)
   556  	c.Assert(err, IsNil)
   557  
   558  	c.Check(osutil.FileExists(filepath.Join(outDir, "foo")), Equals, false)
   559  	// overwritten
   560  	c.Check(filepath.Join(outDir, "bar"), testutil.FileEquals, "foo foo foo")
   561  }
   562  
   563  func (s *mountedfilesystemTestSuite) TestMountedWriterErrorNested(c *C) {
   564  	makeGadgetData(c, s.dir, []gadgetData{
   565  		{name: "foo/foo-dir", content: "data"},
   566  		{name: "foo/bar/baz", content: "data"},
   567  	})
   568  
   569  	ps := &gadget.LaidOutStructure{
   570  		VolumeStructure: &gadget.VolumeStructure{
   571  			Size:       2048,
   572  			Filesystem: "ext4",
   573  			Content: []gadget.VolumeContent{
   574  				{
   575  					// single file in target directory
   576  					UnresolvedSource: "/",
   577  					Target:           "/foo-dir/",
   578  				},
   579  			},
   580  		},
   581  	}
   582  
   583  	outDir := c.MkDir()
   584  
   585  	makeSizedFile(c, filepath.Join(outDir, "/foo-dir/foo/bar"), 0, nil)
   586  
   587  	rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil)
   588  	c.Assert(err, IsNil)
   589  	c.Assert(rw, NotNil)
   590  
   591  	err = rw.Write(outDir, nil)
   592  	c.Assert(err, ErrorMatches, "cannot write filesystem content of source:/: .* not a directory")
   593  }
   594  
   595  func (s *mountedfilesystemTestSuite) TestMountedWriterPreserve(c *C) {
   596  	// some data for the gadget
   597  	gdWritten := []gadgetData{
   598  		{name: "foo", target: "foo-dir/foo", content: "data"},
   599  		{name: "bar", target: "bar-name", content: "data"},
   600  		{name: "boot-assets/splash", target: "splash", content: "data"},
   601  		{name: "boot-assets/some-dir/data", target: "some-dir/data", content: "data"},
   602  		{name: "boot-assets/some-dir/empty-file", target: "some-dir/empty-file", content: "data"},
   603  		{name: "boot-assets/nested-dir/more-nested/more", target: "/nested-copy/more-nested/more", content: "data"},
   604  	}
   605  	gdNotWritten := []gadgetData{
   606  		{name: "foo", target: "/foo", content: "data"},
   607  		{name: "boot-assets/some-dir/data", target: "data-copy", content: "data"},
   608  		{name: "boot-assets/nested-dir/nested", target: "/nested-copy/nested", content: "data"},
   609  	}
   610  	makeGadgetData(c, s.dir, append(gdWritten, gdNotWritten...))
   611  
   612  	// these exist in the root directory and are preserved
   613  	preserve := []string{
   614  		// mix entries with leading / and without
   615  		"/foo",
   616  		"/data-copy",
   617  		"nested-copy/nested",
   618  		"not-listed", // not present in 'gadget' contents
   619  	}
   620  	// these are preserved, but don't exist in the root, so data from gadget
   621  	// will be written
   622  	preserveButNotPresent := []string{
   623  		"/bar-name",
   624  		"some-dir/data",
   625  	}
   626  	outDir := filepath.Join(c.MkDir(), "out-dir")
   627  
   628  	for _, en := range preserve {
   629  		p := filepath.Join(outDir, en)
   630  		makeSizedFile(c, p, 0, []byte("can't touch this"))
   631  	}
   632  
   633  	ps := &gadget.LaidOutStructure{
   634  		VolumeStructure: &gadget.VolumeStructure{
   635  			Size:       2048,
   636  			Filesystem: "ext4",
   637  			Content: []gadget.VolumeContent{
   638  				{
   639  					UnresolvedSource: "foo",
   640  					Target:           "/foo-dir/",
   641  				}, {
   642  					// would overwrite /foo
   643  					UnresolvedSource: "foo",
   644  					Target:           "/",
   645  				}, {
   646  					// preserved, but not present, will be
   647  					// written
   648  					UnresolvedSource: "bar",
   649  					Target:           "/bar-name",
   650  				}, {
   651  					// some-dir/data is preserved, but not
   652  					// preset, hence will be written
   653  					UnresolvedSource: "boot-assets/",
   654  					Target:           "/",
   655  				}, {
   656  					// would overwrite /data-copy
   657  					UnresolvedSource: "boot-assets/some-dir/data",
   658  					Target:           "/data-copy",
   659  				}, {
   660  					// would overwrite /nested-copy/nested
   661  					UnresolvedSource: "boot-assets/nested-dir/",
   662  					Target:           "/nested-copy/",
   663  				},
   664  			},
   665  		},
   666  	}
   667  
   668  	rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil)
   669  	c.Assert(err, IsNil)
   670  	c.Assert(rw, NotNil)
   671  
   672  	err = rw.Write(outDir, append(preserve, preserveButNotPresent...))
   673  	c.Assert(err, IsNil)
   674  
   675  	// files that existed were preserved
   676  	for _, en := range preserve {
   677  		p := filepath.Join(outDir, en)
   678  		c.Check(p, testutil.FileEquals, "can't touch this")
   679  	}
   680  	// everything else was written
   681  	verifyWrittenGadgetData(c, outDir, gdWritten)
   682  }
   683  
   684  func (s *mountedfilesystemTestSuite) TestMountedWriterPreserveWithObserver(c *C) {
   685  	// some data for the gadget
   686  	gd := []gadgetData{
   687  		{name: "foo", target: "foo-dir/foo", content: "foo from gadget"},
   688  	}
   689  	makeGadgetData(c, s.dir, gd)
   690  
   691  	outDir := filepath.Join(c.MkDir(), "out-dir")
   692  	makeSizedFile(c, filepath.Join(outDir, "foo"), 0, []byte("foo from disk"))
   693  
   694  	ps := &gadget.LaidOutStructure{
   695  		VolumeStructure: &gadget.VolumeStructure{
   696  			Size:       2048,
   697  			Filesystem: "ext4",
   698  			Content: []gadget.VolumeContent{
   699  				{
   700  					UnresolvedSource: "foo",
   701  					// would overwrite existing foo
   702  					Target: "foo",
   703  				}, {
   704  					UnresolvedSource: "foo",
   705  					// does not exist
   706  					Target: "foo-new",
   707  				},
   708  			},
   709  		},
   710  	}
   711  
   712  	obs := &mockWriteObserver{
   713  		c:              c,
   714  		expectedStruct: ps,
   715  		preserveTargets: []string{
   716  			"foo",
   717  			"foo-new",
   718  		},
   719  	}
   720  	rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, obs)
   721  	c.Assert(err, IsNil)
   722  	c.Assert(rw, NotNil)
   723  
   724  	err = rw.Write(outDir, nil)
   725  	c.Assert(err, IsNil)
   726  	c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "foo from disk")
   727  	c.Check(filepath.Join(outDir, "foo-new"), testutil.FileAbsent)
   728  }
   729  
   730  func (s *mountedfilesystemTestSuite) TestMountedWriterNonFilePreserveError(c *C) {
   731  	// some data for the gadget
   732  	gd := []gadgetData{
   733  		{name: "foo", content: "data"},
   734  	}
   735  	makeGadgetData(c, s.dir, gd)
   736  
   737  	preserve := []string{
   738  		// this will be a directory
   739  		"foo",
   740  	}
   741  	outDir := filepath.Join(c.MkDir(), "out-dir")
   742  	// will conflict with preserve entry
   743  	err := os.MkdirAll(filepath.Join(outDir, "foo"), 0755)
   744  	c.Assert(err, IsNil)
   745  
   746  	ps := &gadget.LaidOutStructure{
   747  		VolumeStructure: &gadget.VolumeStructure{
   748  			Size:       2048,
   749  			Filesystem: "ext4",
   750  			Content: []gadget.VolumeContent{
   751  				{
   752  					UnresolvedSource: "/",
   753  					Target:           "/",
   754  				},
   755  			},
   756  		},
   757  	}
   758  
   759  	rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil)
   760  	c.Assert(err, IsNil)
   761  	c.Assert(rw, NotNil)
   762  
   763  	err = rw.Write(outDir, preserve)
   764  	c.Assert(err, ErrorMatches, `cannot map preserve entries for destination ".*/out-dir": preserved entry "foo" cannot be a directory`)
   765  }
   766  
   767  func (s *mountedfilesystemTestSuite) TestMountedWriterImplicitDir(c *C) {
   768  	gd := []gadgetData{
   769  		{name: "boot-assets/nested-dir/nested", target: "/nested-copy/nested-dir/nested", content: "nested"},
   770  		{name: "boot-assets/nested-dir/more-nested/more", target: "/nested-copy/nested-dir/more-nested/more", content: "more"},
   771  	}
   772  	makeGadgetData(c, s.dir, gd)
   773  
   774  	ps := &gadget.LaidOutStructure{
   775  		VolumeStructure: &gadget.VolumeStructure{
   776  			Size:       2048,
   777  			Filesystem: "ext4",
   778  			Content: []gadget.VolumeContent{
   779  				{
   780  					// contents of nested directory under new target directory
   781  					UnresolvedSource: "boot-assets/nested-dir",
   782  					Target:           "/nested-copy/",
   783  				},
   784  			},
   785  		},
   786  	}
   787  
   788  	outDir := c.MkDir()
   789  
   790  	rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil)
   791  	c.Assert(err, IsNil)
   792  	c.Assert(rw, NotNil)
   793  
   794  	err = rw.Write(outDir, nil)
   795  	c.Assert(err, IsNil)
   796  
   797  	verifyWrittenGadgetData(c, outDir, gd)
   798  }
   799  
   800  func (s *mountedfilesystemTestSuite) TestMountedWriterNoFs(c *C) {
   801  	ps := &gadget.LaidOutStructure{
   802  		VolumeStructure: &gadget.VolumeStructure{
   803  			Size: 2048,
   804  			// no filesystem
   805  			Content: []gadget.VolumeContent{
   806  				{
   807  					// single file in target directory
   808  					UnresolvedSource: "foo",
   809  					Target:           "/foo-dir/",
   810  				},
   811  			},
   812  		},
   813  	}
   814  
   815  	rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil)
   816  	c.Assert(err, ErrorMatches, "structure #0 has no filesystem")
   817  	c.Assert(rw, IsNil)
   818  }
   819  
   820  func (s *mountedfilesystemTestSuite) TestMountedWriterTrivialValidation(c *C) {
   821  	rw, err := gadget.NewMountedFilesystemWriter(s.dir, nil, nil)
   822  	c.Assert(err, ErrorMatches, `internal error: \*LaidOutStructure.*`)
   823  	c.Assert(rw, IsNil)
   824  
   825  	ps := &gadget.LaidOutStructure{
   826  		VolumeStructure: &gadget.VolumeStructure{
   827  			Size:       2048,
   828  			Filesystem: "ext4",
   829  			// no filesystem
   830  			Content: []gadget.VolumeContent{
   831  				{
   832  					UnresolvedSource: "",
   833  					Target:           "",
   834  				},
   835  			},
   836  		},
   837  	}
   838  
   839  	rw, err = gadget.NewMountedFilesystemWriter("", ps, nil)
   840  	c.Assert(err, ErrorMatches, `internal error: gadget content directory cannot be unset`)
   841  	c.Assert(rw, IsNil)
   842  
   843  	rw, err = gadget.NewMountedFilesystemWriter(s.dir, ps, nil)
   844  	c.Assert(err, IsNil)
   845  
   846  	err = rw.Write("", nil)
   847  	c.Assert(err, ErrorMatches, "internal error: destination directory cannot be unset")
   848  
   849  	d := c.MkDir()
   850  	err = rw.Write(d, nil)
   851  	c.Assert(err, ErrorMatches, "cannot write filesystem content .* source cannot be unset")
   852  
   853  	ps.Content[0].UnresolvedSource = "/"
   854  	err = rw.Write(d, nil)
   855  	c.Assert(err, ErrorMatches, "cannot write filesystem content .* target cannot be unset")
   856  }
   857  
   858  func (s *mountedfilesystemTestSuite) TestMountedWriterSymlinks(c *C) {
   859  	// some data for the gadget
   860  	gd := []gadgetData{
   861  		{name: "foo", target: "foo", content: "data"},
   862  		{name: "nested/foo", target: "nested/foo", content: "nested-data"},
   863  		{name: "link", symlinkTo: "foo"},
   864  		{name: "nested-link", symlinkTo: "nested"},
   865  	}
   866  	makeGadgetData(c, s.dir, gd)
   867  
   868  	outDir := filepath.Join(c.MkDir(), "out-dir")
   869  
   870  	ps := &gadget.LaidOutStructure{
   871  		VolumeStructure: &gadget.VolumeStructure{
   872  			Size:       2048,
   873  			Filesystem: "ext4",
   874  			Content: []gadget.VolumeContent{
   875  				{UnresolvedSource: "/", Target: "/"},
   876  			},
   877  		},
   878  	}
   879  
   880  	rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, nil)
   881  	c.Assert(err, IsNil)
   882  	c.Assert(rw, NotNil)
   883  
   884  	err = rw.Write(outDir, nil)
   885  	c.Assert(err, IsNil)
   886  
   887  	// everything else was written
   888  	verifyWrittenGadgetData(c, outDir, []gadgetData{
   889  		{target: "foo", content: "data"},
   890  		{target: "link", symlinkTo: "foo"},
   891  		{target: "nested/foo", content: "nested-data"},
   892  		{target: "nested-link", symlinkTo: "nested"},
   893  		// when read via symlink
   894  		{target: "nested-link/foo", content: "nested-data"},
   895  	})
   896  }
   897  
   898  type mockContentUpdateObserver struct {
   899  	contentUpdate   map[string][]*mockContentChange
   900  	contentRollback map[string][]*mockContentChange
   901  	preserveTargets []string
   902  	observeErr      error
   903  	expectedStruct  *gadget.LaidOutStructure
   904  	c               *C
   905  }
   906  
   907  func (m *mockContentUpdateObserver) reset() {
   908  	m.contentUpdate = nil
   909  	m.contentRollback = nil
   910  }
   911  
   912  func (m *mockContentUpdateObserver) Observe(op gadget.ContentOperation, sourceStruct *gadget.LaidOutStructure,
   913  	targetRootDir, relativeTargetPath string, data *gadget.ContentChange) (gadget.ContentChangeAction, error) {
   914  	if m.c == nil {
   915  		panic("c is unset")
   916  	}
   917  	if m.contentUpdate == nil {
   918  		m.contentUpdate = make(map[string][]*mockContentChange)
   919  	}
   920  	if m.contentRollback == nil {
   921  		m.contentRollback = make(map[string][]*mockContentChange)
   922  	}
   923  	m.c.Assert(data, NotNil)
   924  
   925  	// the after content must always be set
   926  	m.c.Check(osutil.FileExists(data.After) && !osutil.IsDirectory(data.After), Equals, true,
   927  		Commentf("after reference path %q does not exist or is a directory", data.After))
   928  	// they may be no before content for new files
   929  	if data.Before != "" {
   930  		m.c.Check(osutil.FileExists(data.Before) && !osutil.IsDirectory(data.Before), Equals, true,
   931  			Commentf("before reference path %q does not exist or is a directory", data.Before))
   932  	}
   933  	m.c.Check(filepath.IsAbs(relativeTargetPath), Equals, false,
   934  		Commentf("target path %q is absolute", relativeTargetPath))
   935  
   936  	opData := &mockContentChange{path: relativeTargetPath, change: data}
   937  	switch op {
   938  	case gadget.ContentUpdate:
   939  		m.contentUpdate[targetRootDir] = append(m.contentUpdate[targetRootDir], opData)
   940  	case gadget.ContentRollback:
   941  		m.contentRollback[targetRootDir] = append(m.contentRollback[targetRootDir], opData)
   942  	default:
   943  		m.c.Fatalf("unexpected observe operation %v", op)
   944  	}
   945  
   946  	m.c.Assert(sourceStruct, NotNil)
   947  	m.c.Check(m.expectedStruct, DeepEquals, sourceStruct)
   948  
   949  	if m.observeErr != nil {
   950  		return gadget.ChangeAbort, m.observeErr
   951  	}
   952  	if strutil.ListContains(m.preserveTargets, relativeTargetPath) {
   953  		return gadget.ChangeIgnore, nil
   954  	}
   955  	return gadget.ChangeApply, nil
   956  }
   957  
   958  func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupSimple(c *C) {
   959  	// some data for the gadget
   960  	gdWritten := []gadgetData{
   961  		{name: "bar", target: "bar-name", content: "data"},
   962  		{name: "foo", target: "foo", content: "data"},
   963  		{name: "zed", target: "zed", content: "data"},
   964  		{name: "same-data", target: "same", content: "same"},
   965  		// not included in volume contents
   966  		{name: "not-written", target: "not-written", content: "data"},
   967  	}
   968  	makeGadgetData(c, s.dir, gdWritten)
   969  
   970  	outDir := filepath.Join(c.MkDir(), "out-dir")
   971  
   972  	// these exist in the destination directory and will be backed up
   973  	backedUp := []gadgetData{
   974  		{target: "foo", content: "can't touch this"},
   975  		{target: "nested/foo", content: "can't touch this"},
   976  		// listed in preserve
   977  		{target: "zed", content: "preserved"},
   978  		// same content as the update
   979  		{target: "same", content: "same"},
   980  	}
   981  	makeExistingData(c, outDir, backedUp)
   982  
   983  	ps := &gadget.LaidOutStructure{
   984  		VolumeStructure: &gadget.VolumeStructure{
   985  			Size:       2048,
   986  			Filesystem: "ext4",
   987  			Content: []gadget.VolumeContent{
   988  				{
   989  					UnresolvedSource: "bar",
   990  					Target:           "/bar-name",
   991  				}, {
   992  					UnresolvedSource: "foo",
   993  					Target:           "/",
   994  				}, {
   995  					UnresolvedSource: "foo",
   996  					Target:           "/nested/",
   997  				}, {
   998  					UnresolvedSource: "zed",
   999  					Target:           "/",
  1000  				}, {
  1001  					UnresolvedSource: "same-data",
  1002  					Target:           "/same",
  1003  				},
  1004  			},
  1005  			Update: gadget.VolumeUpdate{
  1006  				Edition:  1,
  1007  				Preserve: []string{"/zed"},
  1008  			},
  1009  		},
  1010  	}
  1011  
  1012  	muo := &mockContentUpdateObserver{
  1013  		c:              c,
  1014  		expectedStruct: ps,
  1015  	}
  1016  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  1017  		c.Check(to, DeepEquals, ps)
  1018  		return outDir, nil
  1019  	}, muo)
  1020  	c.Assert(err, IsNil)
  1021  	c.Assert(rw, NotNil)
  1022  
  1023  	err = rw.Backup()
  1024  	c.Assert(err, IsNil)
  1025  
  1026  	// files that existed were backed up
  1027  	for _, en := range backedUp {
  1028  		backup := filepath.Join(s.backup, "struct-0", en.target+".backup")
  1029  		same := filepath.Join(s.backup, "struct-0", en.target+".same")
  1030  		switch en.content {
  1031  		case "preserved":
  1032  			c.Check(osutil.FileExists(backup), Equals, false, Commentf("file: %v", backup))
  1033  			c.Check(osutil.FileExists(same), Equals, false, Commentf("file: %v", same))
  1034  		case "same":
  1035  			c.Check(osutil.FileExists(same), Equals, true, Commentf("file: %v", same))
  1036  		default:
  1037  			c.Check(backup, testutil.FileEquals, "can't touch this")
  1038  		}
  1039  	}
  1040  	// notified for both updated and new content
  1041  	c.Check(muo.contentUpdate, DeepEquals, map[string][]*mockContentChange{
  1042  		outDir: {
  1043  			// bar-name is a new file
  1044  			{"bar-name", &gadget.ContentChange{
  1045  				After: filepath.Join(s.dir, "bar"),
  1046  			}},
  1047  			// updates
  1048  			{"foo", &gadget.ContentChange{
  1049  				After:  filepath.Join(s.dir, "foo"),
  1050  				Before: filepath.Join(s.backup, "struct-0/foo.backup"),
  1051  			}},
  1052  			{"nested/foo", &gadget.ContentChange{
  1053  				After:  filepath.Join(s.dir, "foo"),
  1054  				Before: filepath.Join(s.backup, "struct-0/nested/foo.backup"),
  1055  			}},
  1056  		},
  1057  	})
  1058  
  1059  	// running backup again (eg. after a reboot) does not error out
  1060  	err = rw.Backup()
  1061  	c.Assert(err, IsNil)
  1062  	// we are notified of all files again
  1063  	c.Check(muo.contentUpdate, DeepEquals, map[string][]*mockContentChange{
  1064  		outDir: {
  1065  			// bar-name is a new file
  1066  			{"bar-name", &gadget.ContentChange{
  1067  				After: filepath.Join(s.dir, "bar"),
  1068  			}},
  1069  			{"foo", &gadget.ContentChange{
  1070  				After:  filepath.Join(s.dir, "foo"),
  1071  				Before: filepath.Join(s.backup, "struct-0/foo.backup"),
  1072  			}},
  1073  			{"nested/foo", &gadget.ContentChange{
  1074  				After:  filepath.Join(s.dir, "foo"),
  1075  				Before: filepath.Join(s.backup, "struct-0/nested/foo.backup"),
  1076  			}},
  1077  			// same set of calls once more
  1078  			{"bar-name", &gadget.ContentChange{
  1079  				After: filepath.Join(s.dir, "bar"),
  1080  			}},
  1081  			{"foo", &gadget.ContentChange{
  1082  				After:  filepath.Join(s.dir, "foo"),
  1083  				Before: filepath.Join(s.backup, "struct-0/foo.backup"),
  1084  			}},
  1085  			{"nested/foo", &gadget.ContentChange{
  1086  				After:  filepath.Join(s.dir, "foo"),
  1087  				Before: filepath.Join(s.backup, "struct-0/nested/foo.backup"),
  1088  			}},
  1089  		},
  1090  	})
  1091  }
  1092  
  1093  func (s *mountedfilesystemTestSuite) TestMountedWriterObserverErr(c *C) {
  1094  	makeGadgetData(c, s.dir, []gadgetData{
  1095  		{name: "foo", content: "data"},
  1096  	})
  1097  
  1098  	ps := &gadget.LaidOutStructure{
  1099  		VolumeStructure: &gadget.VolumeStructure{
  1100  			Size:       2048,
  1101  			Filesystem: "ext4",
  1102  			Content: []gadget.VolumeContent{
  1103  				{UnresolvedSource: "/", Target: "/"},
  1104  			},
  1105  		},
  1106  	}
  1107  
  1108  	outDir := c.MkDir()
  1109  	obs := &mockWriteObserver{
  1110  		c:              c,
  1111  		observeErr:     errors.New("observe fail"),
  1112  		expectedStruct: ps,
  1113  	}
  1114  	rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps, obs)
  1115  	c.Assert(err, IsNil)
  1116  	c.Assert(rw, NotNil)
  1117  
  1118  	err = rw.Write(outDir, nil)
  1119  	c.Assert(err, ErrorMatches, "cannot write filesystem content of source:/: cannot observe file write: observe fail")
  1120  }
  1121  
  1122  func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupWithDirectories(c *C) {
  1123  	// some data for the gadget
  1124  	gdWritten := []gadgetData{
  1125  		{name: "bar", content: "data"},
  1126  		{name: "some-dir/foo", content: "data"},
  1127  		{name: "empty-dir/"},
  1128  	}
  1129  	makeGadgetData(c, s.dir, gdWritten)
  1130  
  1131  	outDir := filepath.Join(c.MkDir(), "out-dir")
  1132  
  1133  	// these exist in the destination directory and will be backed up
  1134  	backedUp := []gadgetData{
  1135  		// overwritten by "bar" -> "/foo"
  1136  		{target: "foo", content: "can't touch this"},
  1137  		// overwritten by some-dir/ -> /nested/
  1138  		{target: "nested/foo", content: "can't touch this"},
  1139  		// written to by bar -> /this/is/some/nested/
  1140  		{target: "this/is/some/"},
  1141  		{target: "lone-dir/"},
  1142  	}
  1143  	makeExistingData(c, outDir, backedUp)
  1144  
  1145  	ps := &gadget.LaidOutStructure{
  1146  		VolumeStructure: &gadget.VolumeStructure{
  1147  			Size:       2048,
  1148  			Filesystem: "ext4",
  1149  			Content: []gadget.VolumeContent{
  1150  				{
  1151  					UnresolvedSource: "bar",
  1152  					Target:           "/foo",
  1153  				}, {
  1154  					UnresolvedSource: "bar",
  1155  					Target:           "/this/is/some/nested/",
  1156  				}, {
  1157  					UnresolvedSource: "some-dir/",
  1158  					Target:           "/nested/",
  1159  				}, {
  1160  					UnresolvedSource: "empty-dir/",
  1161  					Target:           "/lone-dir/",
  1162  				},
  1163  			},
  1164  			Update: gadget.VolumeUpdate{
  1165  				Edition:  1,
  1166  				Preserve: []string{"/zed"},
  1167  			},
  1168  		},
  1169  	}
  1170  
  1171  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  1172  		c.Check(to, DeepEquals, ps)
  1173  		return outDir, nil
  1174  	}, nil)
  1175  	c.Assert(err, IsNil)
  1176  	c.Assert(rw, NotNil)
  1177  
  1178  	err = rw.Backup()
  1179  	c.Assert(err, IsNil)
  1180  
  1181  	verifyDirContents(c, filepath.Join(s.backup, "struct-0"), map[string]contentType{
  1182  		"this/is/some.backup": typeFile,
  1183  		"this/is.backup":      typeFile,
  1184  		"this.backup":         typeFile,
  1185  
  1186  		"nested/foo.backup": typeFile,
  1187  		"nested.backup":     typeFile,
  1188  
  1189  		"foo.backup": typeFile,
  1190  
  1191  		"lone-dir.backup": typeFile,
  1192  	})
  1193  }
  1194  
  1195  func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupNonexistent(c *C) {
  1196  	// some data for the gadget
  1197  	gd := []gadgetData{
  1198  		{name: "bar", target: "foo", content: "data"},
  1199  		{name: "bar", target: "some-dir/foo", content: "data"},
  1200  	}
  1201  	makeGadgetData(c, s.dir, gd)
  1202  
  1203  	outDir := filepath.Join(c.MkDir(), "out-dir")
  1204  
  1205  	ps := &gadget.LaidOutStructure{
  1206  		VolumeStructure: &gadget.VolumeStructure{
  1207  			Size:       2048,
  1208  			Filesystem: "ext4",
  1209  			Content: []gadget.VolumeContent{
  1210  				{
  1211  					UnresolvedSource: "bar",
  1212  					Target:           "/foo",
  1213  				}, {
  1214  					UnresolvedSource: "bar",
  1215  					Target:           "/some-dir/foo",
  1216  				},
  1217  			},
  1218  			Update: gadget.VolumeUpdate{
  1219  				Edition: 1,
  1220  				// bar not in preserved files
  1221  			},
  1222  		},
  1223  	}
  1224  
  1225  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  1226  		c.Check(to, DeepEquals, ps)
  1227  		return outDir, nil
  1228  	}, nil)
  1229  	c.Assert(err, IsNil)
  1230  	c.Assert(rw, NotNil)
  1231  
  1232  	err = rw.Backup()
  1233  	c.Assert(err, IsNil)
  1234  
  1235  	backupRoot := filepath.Join(s.backup, "struct-0")
  1236  	// actually empty
  1237  	verifyDirContents(c, backupRoot, map[string]contentType{})
  1238  }
  1239  
  1240  func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupFailsOnBackupDirErrors(c *C) {
  1241  	if os.Geteuid() == 0 {
  1242  		c.Skip("the test cannot be run by the root user")
  1243  	}
  1244  
  1245  	outDir := filepath.Join(c.MkDir(), "out-dir")
  1246  
  1247  	ps := &gadget.LaidOutStructure{
  1248  		VolumeStructure: &gadget.VolumeStructure{
  1249  			Size:       2048,
  1250  			Filesystem: "ext4",
  1251  			Content: []gadget.VolumeContent{
  1252  				{
  1253  					UnresolvedSource: "bar",
  1254  					Target:           "/foo",
  1255  				},
  1256  			},
  1257  			Update: gadget.VolumeUpdate{
  1258  				Edition: 1,
  1259  			},
  1260  		},
  1261  	}
  1262  
  1263  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  1264  		c.Check(to, DeepEquals, ps)
  1265  		return outDir, nil
  1266  	}, nil)
  1267  	c.Assert(err, IsNil)
  1268  	c.Assert(rw, NotNil)
  1269  
  1270  	err = os.Chmod(s.backup, 0555)
  1271  	c.Assert(err, IsNil)
  1272  	defer os.Chmod(s.backup, 0755)
  1273  
  1274  	err = rw.Backup()
  1275  	c.Assert(err, ErrorMatches, "cannot create backup directory: .*/struct-0: permission denied")
  1276  }
  1277  
  1278  func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupFailsOnDestinationErrors(c *C) {
  1279  	if os.Geteuid() == 0 {
  1280  		c.Skip("the test cannot be run by the root user")
  1281  	}
  1282  
  1283  	// some data for the gadget
  1284  	gd := []gadgetData{
  1285  		{name: "bar", content: "data"},
  1286  	}
  1287  	makeGadgetData(c, s.dir, gd)
  1288  
  1289  	outDir := filepath.Join(c.MkDir(), "out-dir")
  1290  	makeExistingData(c, outDir, []gadgetData{
  1291  		{target: "foo", content: "same"},
  1292  	})
  1293  
  1294  	err := os.Chmod(filepath.Join(outDir, "foo"), 0000)
  1295  	c.Assert(err, IsNil)
  1296  
  1297  	ps := &gadget.LaidOutStructure{
  1298  		VolumeStructure: &gadget.VolumeStructure{
  1299  			Size:       2048,
  1300  			Filesystem: "ext4",
  1301  			Content: []gadget.VolumeContent{
  1302  				{
  1303  					UnresolvedSource: "bar",
  1304  					Target:           "/foo",
  1305  				},
  1306  			},
  1307  			Update: gadget.VolumeUpdate{
  1308  				Edition: 1,
  1309  			},
  1310  		},
  1311  	}
  1312  
  1313  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  1314  		c.Check(to, DeepEquals, ps)
  1315  		return outDir, nil
  1316  	}, nil)
  1317  	c.Assert(err, IsNil)
  1318  	c.Assert(rw, NotNil)
  1319  
  1320  	err = rw.Backup()
  1321  	c.Assert(err, ErrorMatches, "cannot backup content: cannot open destination file: open .*/out-dir/foo: permission denied")
  1322  }
  1323  
  1324  func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupFailsOnBadSrcComparison(c *C) {
  1325  	if os.Geteuid() == 0 {
  1326  		c.Skip("the test cannot be run by the root user")
  1327  	}
  1328  
  1329  	// some data for the gadget
  1330  	gd := []gadgetData{
  1331  		{name: "bar", content: "data"},
  1332  	}
  1333  	makeGadgetData(c, s.dir, gd)
  1334  	err := os.Chmod(filepath.Join(s.dir, "bar"), 0000)
  1335  	c.Assert(err, IsNil)
  1336  
  1337  	outDir := filepath.Join(c.MkDir(), "out-dir")
  1338  	makeExistingData(c, outDir, []gadgetData{
  1339  		{target: "foo", content: "same"},
  1340  	})
  1341  
  1342  	ps := &gadget.LaidOutStructure{
  1343  		VolumeStructure: &gadget.VolumeStructure{
  1344  			Size:       2048,
  1345  			Filesystem: "ext4",
  1346  			Content: []gadget.VolumeContent{
  1347  				{
  1348  					UnresolvedSource: "bar",
  1349  					Target:           "/foo",
  1350  				},
  1351  			},
  1352  			Update: gadget.VolumeUpdate{
  1353  				Edition: 1,
  1354  			},
  1355  		},
  1356  	}
  1357  
  1358  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  1359  		c.Check(to, DeepEquals, ps)
  1360  		return outDir, nil
  1361  	}, nil)
  1362  	c.Assert(err, IsNil)
  1363  	c.Assert(rw, NotNil)
  1364  
  1365  	err = rw.Backup()
  1366  	c.Assert(err, ErrorMatches, "cannot backup content: cannot checksum update file: open .*/bar: permission denied")
  1367  }
  1368  
  1369  func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupFunnyNamesConflictBackup(c *C) {
  1370  	gdWritten := []gadgetData{
  1371  		{name: "bar.backup/foo", content: "data"},
  1372  		{name: "bar", content: "data"},
  1373  		{name: "foo.same/foo", content: "same-as-current"},
  1374  		{name: "foo", content: "same-as-current"},
  1375  	}
  1376  	makeGadgetData(c, s.dir, gdWritten)
  1377  
  1378  	// backup stamps conflicts with bar.backup
  1379  	existingUpBar := []gadgetData{
  1380  		// will be listed first
  1381  		{target: "bar", content: "can't touch this"},
  1382  		{target: "bar.backup/foo", content: "can't touch this"},
  1383  	}
  1384  	// backup stamps conflicts with foo.same
  1385  	existingUpFoo := []gadgetData{
  1386  		// will be listed first
  1387  		{target: "foo", content: "same-as-current"},
  1388  		{target: "foo.same/foo", content: "can't touch this"},
  1389  	}
  1390  
  1391  	outDirConflictsBar := filepath.Join(c.MkDir(), "out-dir-bar")
  1392  	makeExistingData(c, outDirConflictsBar, existingUpBar)
  1393  
  1394  	outDirConflictsFoo := filepath.Join(c.MkDir(), "out-dir-foo")
  1395  	makeExistingData(c, outDirConflictsFoo, existingUpFoo)
  1396  
  1397  	ps := &gadget.LaidOutStructure{
  1398  		VolumeStructure: &gadget.VolumeStructure{
  1399  			Size:       2048,
  1400  			Filesystem: "ext4",
  1401  			Content: []gadget.VolumeContent{
  1402  				{UnresolvedSource: "/", Target: "/"},
  1403  			},
  1404  			Update: gadget.VolumeUpdate{
  1405  				Edition: 1,
  1406  			},
  1407  		},
  1408  	}
  1409  
  1410  	backupBar := filepath.Join(s.backup, "backup-bar")
  1411  	backupFoo := filepath.Join(s.backup, "backup-foo")
  1412  
  1413  	prefix := `cannot backup content: cannot create backup file: cannot create stamp file prefix: `
  1414  	for _, tc := range []struct {
  1415  		backupDir string
  1416  		outDir    string
  1417  		err       string
  1418  	}{
  1419  		{backupBar, outDirConflictsBar, prefix + `mkdir .*/bar.backup: not a directory`},
  1420  		{backupFoo, outDirConflictsFoo, prefix + `mkdir .*/foo.same: not a directory`},
  1421  	} {
  1422  		err := os.MkdirAll(tc.backupDir, 0755)
  1423  		c.Assert(err, IsNil)
  1424  		rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, tc.backupDir, func(to *gadget.LaidOutStructure) (string, error) {
  1425  			c.Check(to, DeepEquals, ps)
  1426  			return tc.outDir, nil
  1427  		}, nil)
  1428  		c.Assert(err, IsNil)
  1429  		c.Assert(rw, NotNil)
  1430  
  1431  		err = rw.Backup()
  1432  		c.Assert(err, ErrorMatches, tc.err)
  1433  	}
  1434  }
  1435  
  1436  func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupFunnyNamesOk(c *C) {
  1437  	gdWritten := []gadgetData{
  1438  		{name: "bar.backup/foo", target: "bar.backup/foo", content: "data"},
  1439  		{name: "foo.same/foo.same", target: "foo.same/foo.same", content: "same-as-current"},
  1440  		{name: "zed.preserve", target: "zed.preserve", content: "this-is-preserved"},
  1441  		{name: "new-file.same", target: "new-file.same", content: "this-is-new"},
  1442  	}
  1443  	makeGadgetData(c, s.dir, gdWritten)
  1444  
  1445  	outDir := filepath.Join(c.MkDir(), "out-dir")
  1446  
  1447  	// these exist in the destination directory and will be backed up
  1448  	backedUp := []gadgetData{
  1449  		// will be listed first
  1450  		{target: "bar.backup/foo", content: "not-data"},
  1451  		{target: "foo.same/foo.same", content: "same-as-current"},
  1452  		{target: "zed.preserve", content: "to-be-preserved"},
  1453  	}
  1454  	makeExistingData(c, outDir, backedUp)
  1455  
  1456  	ps := &gadget.LaidOutStructure{
  1457  		VolumeStructure: &gadget.VolumeStructure{
  1458  			Size:       2048,
  1459  			Filesystem: "ext4",
  1460  			Content: []gadget.VolumeContent{
  1461  				{UnresolvedSource: "/", Target: "/"},
  1462  			},
  1463  			Update: gadget.VolumeUpdate{
  1464  				Edition: 1,
  1465  				Preserve: []string{
  1466  					"zed.preserve",
  1467  				},
  1468  			},
  1469  		},
  1470  	}
  1471  
  1472  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  1473  		c.Check(to, DeepEquals, ps)
  1474  		return outDir, nil
  1475  	}, nil)
  1476  	c.Assert(err, IsNil)
  1477  	c.Assert(rw, NotNil)
  1478  
  1479  	err = rw.Backup()
  1480  	c.Assert(err, IsNil)
  1481  
  1482  	verifyDirContents(c, filepath.Join(s.backup, "struct-0"), map[string]contentType{
  1483  		"bar.backup.backup":     typeFile,
  1484  		"bar.backup/foo.backup": typeFile,
  1485  
  1486  		"foo.same.backup":        typeFile,
  1487  		"foo.same/foo.same.same": typeFile,
  1488  
  1489  		"zed.preserve.preserve": typeFile,
  1490  	})
  1491  }
  1492  
  1493  func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupErrorOnSymlinkFile(c *C) {
  1494  	gd := []gadgetData{
  1495  		{name: "bar/data", target: "bar/data", content: "some data"},
  1496  		{name: "bar/foo", target: "bar/foo", content: "data"},
  1497  	}
  1498  	makeGadgetData(c, s.dir, gd)
  1499  
  1500  	outDir := filepath.Join(c.MkDir(), "out-dir")
  1501  
  1502  	existing := []gadgetData{
  1503  		{target: "bar/data", content: "some data"},
  1504  		{target: "bar/foo", symlinkTo: "data"},
  1505  	}
  1506  	makeExistingData(c, outDir, existing)
  1507  
  1508  	ps := &gadget.LaidOutStructure{
  1509  		VolumeStructure: &gadget.VolumeStructure{
  1510  			Size:       2048,
  1511  			Filesystem: "ext4",
  1512  			Content: []gadget.VolumeContent{
  1513  				{UnresolvedSource: "/", Target: "/"},
  1514  			},
  1515  		},
  1516  	}
  1517  
  1518  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  1519  		c.Check(to, DeepEquals, ps)
  1520  		return outDir, nil
  1521  	}, nil)
  1522  	c.Assert(err, IsNil)
  1523  	c.Assert(rw, NotNil)
  1524  
  1525  	err = rw.Backup()
  1526  	c.Assert(err, ErrorMatches, "cannot backup content: cannot backup file /bar/foo: symbolic links are not supported")
  1527  }
  1528  
  1529  func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupErrorOnSymlinkInPrefixDir(c *C) {
  1530  	gd := []gadgetData{
  1531  		{name: "bar/nested/data", target: "bar/data", content: "some data"},
  1532  		{name: "baz/foo", target: "baz/foo", content: "data"},
  1533  	}
  1534  	makeGadgetData(c, s.dir, gd)
  1535  
  1536  	outDir := filepath.Join(c.MkDir(), "out-dir")
  1537  
  1538  	existing := []gadgetData{
  1539  		{target: "bar/nested-target/data", content: "some data"},
  1540  	}
  1541  	makeExistingData(c, outDir, existing)
  1542  	// bar/nested-target -> nested
  1543  	os.Symlink("nested-target", filepath.Join(outDir, "bar/nested"))
  1544  
  1545  	ps := &gadget.LaidOutStructure{
  1546  		VolumeStructure: &gadget.VolumeStructure{
  1547  			Size:       2048,
  1548  			Filesystem: "ext4",
  1549  			Content: []gadget.VolumeContent{
  1550  				{UnresolvedSource: "/", Target: "/"},
  1551  			},
  1552  		},
  1553  	}
  1554  
  1555  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  1556  		c.Check(to, DeepEquals, ps)
  1557  		return outDir, nil
  1558  	}, nil)
  1559  	c.Assert(err, IsNil)
  1560  	c.Assert(rw, NotNil)
  1561  
  1562  	err = rw.Backup()
  1563  	c.Assert(err, ErrorMatches, "cannot backup content: cannot create a checkpoint for directory /bar/nested: symbolic links are not supported")
  1564  }
  1565  
  1566  func (s *mountedfilesystemTestSuite) TestMountedUpdaterUpdate(c *C) {
  1567  	// some data for the gadget
  1568  	gdWritten := []gadgetData{
  1569  		{name: "foo", target: "foo-dir/foo", content: "data"},
  1570  		{name: "bar", target: "bar-name", content: "data"},
  1571  		{name: "boot-assets/splash", target: "splash", content: "data"},
  1572  		{name: "boot-assets/some-dir/data", target: "some-dir/data", content: "data"},
  1573  		{name: "boot-assets/some-dir/empty-file", target: "some-dir/empty-file", content: ""},
  1574  		{name: "boot-assets/nested-dir/more-nested/more", target: "/nested-copy/more-nested/more", content: "data"},
  1575  	}
  1576  	// data inside the gadget that will be skipped due to being part of
  1577  	// 'preserve' list
  1578  	gdNotWritten := []gadgetData{
  1579  		{name: "foo", target: "/foo", content: "data"},
  1580  		{name: "boot-assets/some-dir/data", target: "data-copy", content: "data"},
  1581  		{name: "boot-assets/nested-dir/nested", target: "/nested-copy/nested", content: "data"},
  1582  	}
  1583  	// data inside the gadget that is identical to what is already present in the target
  1584  	gdIdentical := []gadgetData{
  1585  		{name: "boot-assets/nested-dir/more-nested/identical", target: "/nested-copy/more-nested/identical", content: "same-as-target"},
  1586  		{name: "boot-assets/nested-dir/same-as-target-dir/identical", target: "/nested-copy/same-as-target-dir/identical", content: "same-as-target"},
  1587  	}
  1588  
  1589  	gd := append(gdWritten, gdNotWritten...)
  1590  	gd = append(gd, gdIdentical...)
  1591  	makeGadgetData(c, s.dir, gd)
  1592  
  1593  	// these exist in the root directory and are preserved
  1594  	preserve := []string{
  1595  		// mix entries with leading / and without
  1596  		"/foo",
  1597  		"/data-copy",
  1598  		"nested-copy/nested",
  1599  		"not-listed", // not present in 'gadget' contents
  1600  	}
  1601  	// these are preserved, but don't exist in the root, so data from gadget
  1602  	// will be written
  1603  	preserveButNotPresent := []string{
  1604  		"/bar-name",
  1605  		"some-dir/data",
  1606  	}
  1607  	outDir := filepath.Join(c.MkDir(), "out-dir")
  1608  
  1609  	for _, en := range preserve {
  1610  		p := filepath.Join(outDir, en)
  1611  		makeSizedFile(c, p, 0, []byte("can't touch this"))
  1612  	}
  1613  	for _, en := range gdIdentical {
  1614  		makeSizedFile(c, filepath.Join(outDir, en.target), 0, []byte(en.content))
  1615  	}
  1616  
  1617  	ps := &gadget.LaidOutStructure{
  1618  		VolumeStructure: &gadget.VolumeStructure{
  1619  			Size:       2048,
  1620  			Filesystem: "ext4",
  1621  			Content: []gadget.VolumeContent{
  1622  				{
  1623  					UnresolvedSource: "foo",
  1624  					Target:           "/foo-dir/",
  1625  				}, {
  1626  					// would overwrite /foo
  1627  					UnresolvedSource: "foo",
  1628  					Target:           "/",
  1629  				}, {
  1630  					// preserved, but not present, will be
  1631  					// written
  1632  					UnresolvedSource: "bar",
  1633  					Target:           "/bar-name",
  1634  				}, {
  1635  					// some-dir/data is preserved, but not
  1636  					// present, hence will be written
  1637  					UnresolvedSource: "boot-assets/",
  1638  					Target:           "/",
  1639  				}, {
  1640  					// would overwrite /data-copy
  1641  					UnresolvedSource: "boot-assets/some-dir/data",
  1642  					Target:           "/data-copy",
  1643  				}, {
  1644  					// would overwrite /nested-copy/nested
  1645  					UnresolvedSource: "boot-assets/nested-dir/",
  1646  					Target:           "/nested-copy/",
  1647  				}, {
  1648  					UnresolvedSource: "boot-assets",
  1649  					Target:           "/boot-assets-copy/",
  1650  				},
  1651  			},
  1652  			Update: gadget.VolumeUpdate{
  1653  				Edition:  1,
  1654  				Preserve: append(preserve, preserveButNotPresent...),
  1655  			},
  1656  		},
  1657  	}
  1658  
  1659  	muo := &mockContentUpdateObserver{
  1660  		c:              c,
  1661  		expectedStruct: ps,
  1662  	}
  1663  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  1664  		c.Check(to, DeepEquals, ps)
  1665  		return outDir, nil
  1666  	}, muo)
  1667  	c.Assert(err, IsNil)
  1668  	c.Assert(rw, NotNil)
  1669  
  1670  	err = rw.Backup()
  1671  	c.Assert(err, IsNil)
  1672  
  1673  	// identical files were identified as such
  1674  	for _, en := range gdIdentical {
  1675  		c.Check(filepath.Join(s.backup, "struct-0", en.target)+".same", testutil.FilePresent)
  1676  	}
  1677  
  1678  	// only notified about content getting updated
  1679  	c.Check(muo.contentUpdate, DeepEquals, map[string][]*mockContentChange{
  1680  		outDir: {
  1681  			// the following files were not observed because they
  1682  			// are the same as the ones on disk:
  1683  			// - nested-copy/more-nested/identical
  1684  			// - nested-copy/same-as-target-dir/identical
  1685  			//
  1686  			// we still get notified about new files:
  1687  			{"foo-dir/foo", &gadget.ContentChange{
  1688  				After: filepath.Join(s.dir, "foo"),
  1689  			}},
  1690  			// in the preserve list but not present
  1691  			{"bar-name", &gadget.ContentChange{
  1692  				After: filepath.Join(s.dir, "bar"),
  1693  			}},
  1694  			// boot-assets/ -> /
  1695  			{"nested-dir/more-nested/identical", &gadget.ContentChange{
  1696  				After: filepath.Join(s.dir, "boot-assets/nested-dir/more-nested/identical"),
  1697  			}},
  1698  			{"nested-dir/more-nested/more", &gadget.ContentChange{
  1699  				After: filepath.Join(s.dir, "boot-assets/nested-dir/more-nested/more"),
  1700  			}},
  1701  			{"nested-dir/nested", &gadget.ContentChange{
  1702  				After: filepath.Join(s.dir, "boot-assets/nested-dir/nested"),
  1703  			}},
  1704  			{"nested-dir/same-as-target-dir/identical", &gadget.ContentChange{
  1705  				After: filepath.Join(s.dir, "boot-assets/nested-dir/same-as-target-dir/identical"),
  1706  			}},
  1707  			// in the preserve list but not present
  1708  			{"some-dir/data", &gadget.ContentChange{
  1709  				After: filepath.Join(s.dir, "boot-assets/some-dir/data"),
  1710  			}},
  1711  			{"some-dir/empty-file", &gadget.ContentChange{
  1712  				After: filepath.Join(s.dir, "boot-assets/some-dir/empty-file"),
  1713  			}},
  1714  			{"splash", &gadget.ContentChange{
  1715  				After: filepath.Join(s.dir, "boot-assets/splash"),
  1716  			}},
  1717  			// boot-assets/nested-dir/ -> /nested-copy/
  1718  			{"nested-copy/more-nested/more", &gadget.ContentChange{
  1719  				After: filepath.Join(s.dir, "boot-assets/nested-dir/more-nested/more"),
  1720  			}},
  1721  			// boot-assets -> /boot-assets-copy/
  1722  			{"boot-assets-copy/boot-assets/nested-dir/more-nested/identical", &gadget.ContentChange{
  1723  				After: filepath.Join(s.dir, "boot-assets/nested-dir/more-nested/identical")}},
  1724  			{"boot-assets-copy/boot-assets/nested-dir/more-nested/more", &gadget.ContentChange{
  1725  				After: filepath.Join(s.dir, "boot-assets/nested-dir/more-nested/more")}},
  1726  			{"boot-assets-copy/boot-assets/nested-dir/nested", &gadget.ContentChange{
  1727  				After: filepath.Join(s.dir, "boot-assets/nested-dir/nested"),
  1728  			}},
  1729  			{"boot-assets-copy/boot-assets/nested-dir/same-as-target-dir/identical", &gadget.ContentChange{
  1730  				After: filepath.Join(s.dir, "boot-assets/nested-dir/same-as-target-dir/identical"),
  1731  			}},
  1732  			{"boot-assets-copy/boot-assets/some-dir/data", &gadget.ContentChange{
  1733  				After: filepath.Join(s.dir, "boot-assets/some-dir/data"),
  1734  			}},
  1735  			{"boot-assets-copy/boot-assets/some-dir/empty-file", &gadget.ContentChange{
  1736  				After: filepath.Join(s.dir, "boot-assets/some-dir/empty-file"),
  1737  			}},
  1738  			{"boot-assets-copy/boot-assets/splash", &gadget.ContentChange{
  1739  				After: filepath.Join(s.dir, "boot-assets/splash"),
  1740  			}},
  1741  		},
  1742  	})
  1743  
  1744  	err = rw.Update()
  1745  	c.Assert(err, IsNil)
  1746  
  1747  	// files that existed were preserved
  1748  	for _, en := range preserve {
  1749  		p := filepath.Join(outDir, en)
  1750  		c.Check(p, testutil.FileEquals, "can't touch this")
  1751  	}
  1752  	// everything else was written
  1753  	verifyWrittenGadgetData(c, outDir, append(gdWritten, gdIdentical...))
  1754  }
  1755  
  1756  func (s *mountedfilesystemTestSuite) TestMountedUpdaterDirContents(c *C) {
  1757  	// some data for the gadget
  1758  	gdWritten := []gadgetData{
  1759  		{name: "bar/foo", target: "/bar-name/foo", content: "data"},
  1760  		{name: "bar/nested/foo", target: "/bar-name/nested/foo", content: "data"},
  1761  		{name: "bar/foo", target: "/bar-copy/bar/foo", content: "data"},
  1762  		{name: "bar/nested/foo", target: "/bar-copy/bar/nested/foo", content: "data"},
  1763  		{name: "deep-nested", target: "/this/is/some/deep/nesting/deep-nested", content: "data"},
  1764  	}
  1765  	makeGadgetData(c, s.dir, gdWritten)
  1766  
  1767  	outDir := filepath.Join(c.MkDir(), "out-dir")
  1768  
  1769  	ps := &gadget.LaidOutStructure{
  1770  		VolumeStructure: &gadget.VolumeStructure{
  1771  			Size:       2048,
  1772  			Filesystem: "ext4",
  1773  			Content: []gadget.VolumeContent{
  1774  				{
  1775  					// contents of bar under /bar-name/
  1776  					UnresolvedSource: "bar/",
  1777  					Target:           "/bar-name",
  1778  				}, {
  1779  					// whole bar under /bar-copy/
  1780  					UnresolvedSource: "bar",
  1781  					Target:           "/bar-copy/",
  1782  				}, {
  1783  					// deep prefix
  1784  					UnresolvedSource: "deep-nested",
  1785  					Target:           "/this/is/some/deep/nesting/",
  1786  				},
  1787  			},
  1788  			Update: gadget.VolumeUpdate{
  1789  				Edition: 1,
  1790  			},
  1791  		},
  1792  	}
  1793  
  1794  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  1795  		c.Check(to, DeepEquals, ps)
  1796  		return outDir, nil
  1797  	}, nil)
  1798  	c.Assert(err, IsNil)
  1799  	c.Assert(rw, NotNil)
  1800  
  1801  	err = rw.Backup()
  1802  	c.Assert(err, IsNil)
  1803  
  1804  	err = rw.Update()
  1805  	c.Assert(err, IsNil)
  1806  
  1807  	verifyWrittenGadgetData(c, outDir, gdWritten)
  1808  }
  1809  
  1810  func (s *mountedfilesystemTestSuite) TestMountedUpdaterExpectsBackup(c *C) {
  1811  	// some data for the gadget
  1812  	gd := []gadgetData{
  1813  		{name: "bar", target: "foo", content: "update"},
  1814  		{name: "bar", target: "some-dir/foo", content: "update"},
  1815  	}
  1816  	makeGadgetData(c, s.dir, gd)
  1817  
  1818  	outDir := filepath.Join(c.MkDir(), "out-dir")
  1819  	makeExistingData(c, outDir, []gadgetData{
  1820  		{target: "foo", content: "content"},
  1821  		{target: "some-dir/foo", content: "content"},
  1822  		{target: "/preserved", content: "preserve"},
  1823  	})
  1824  
  1825  	ps := &gadget.LaidOutStructure{
  1826  		VolumeStructure: &gadget.VolumeStructure{
  1827  			Size:       2048,
  1828  			Filesystem: "ext4",
  1829  			Content: []gadget.VolumeContent{
  1830  				{
  1831  					UnresolvedSource: "bar",
  1832  					Target:           "/foo",
  1833  				}, {
  1834  					UnresolvedSource: "bar",
  1835  					Target:           "/some-dir/foo",
  1836  				}, {
  1837  					UnresolvedSource: "bar",
  1838  					Target:           "/preserved",
  1839  				},
  1840  			},
  1841  			Update: gadget.VolumeUpdate{
  1842  				Edition: 1,
  1843  				// bar not in preserved files
  1844  				Preserve: []string{"preserved"},
  1845  			},
  1846  		},
  1847  	}
  1848  
  1849  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  1850  		c.Check(to, DeepEquals, ps)
  1851  		return outDir, nil
  1852  	}, nil)
  1853  	c.Assert(err, IsNil)
  1854  	c.Assert(rw, NotNil)
  1855  
  1856  	err = rw.Update()
  1857  	c.Assert(err, ErrorMatches, `cannot update content: missing backup file ".*/struct-0/foo.backup" for /foo`)
  1858  	// create a mock backup of first file
  1859  	makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.backup"), 0, nil)
  1860  	// try again
  1861  	err = rw.Update()
  1862  	c.Assert(err, ErrorMatches, `cannot update content: missing backup file ".*/struct-0/some-dir/foo.backup" for /some-dir/foo`)
  1863  	// create a mock backup of second entry
  1864  	makeSizedFile(c, filepath.Join(s.backup, "struct-0/some-dir/foo.backup"), 0, nil)
  1865  	// try again (preserved files need no backup)
  1866  	err = rw.Update()
  1867  	c.Assert(err, IsNil)
  1868  
  1869  	verifyWrittenGadgetData(c, outDir, []gadgetData{
  1870  		{target: "foo", content: "update"},
  1871  		{target: "some-dir/foo", content: "update"},
  1872  		{target: "/preserved", content: "preserve"},
  1873  	})
  1874  }
  1875  
  1876  func (s *mountedfilesystemTestSuite) TestMountedUpdaterEmptyDir(c *C) {
  1877  	// some data for the gadget
  1878  	err := os.MkdirAll(filepath.Join(s.dir, "empty-dir"), 0755)
  1879  	c.Assert(err, IsNil)
  1880  	err = os.MkdirAll(filepath.Join(s.dir, "non-empty/empty-dir"), 0755)
  1881  	c.Assert(err, IsNil)
  1882  
  1883  	outDir := filepath.Join(c.MkDir(), "out-dir")
  1884  
  1885  	ps := &gadget.LaidOutStructure{
  1886  		VolumeStructure: &gadget.VolumeStructure{
  1887  			Size:       2048,
  1888  			Filesystem: "ext4",
  1889  			Content: []gadget.VolumeContent{
  1890  				{
  1891  					UnresolvedSource: "/",
  1892  					Target:           "/",
  1893  				}, {
  1894  					UnresolvedSource: "/",
  1895  					Target:           "/foo",
  1896  				}, {
  1897  					UnresolvedSource: "/non-empty/empty-dir/",
  1898  					Target:           "/contents-of-empty/",
  1899  				},
  1900  			},
  1901  			Update: gadget.VolumeUpdate{
  1902  				Edition: 1,
  1903  			},
  1904  		},
  1905  	}
  1906  
  1907  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  1908  		c.Check(to, DeepEquals, ps)
  1909  		return outDir, nil
  1910  	}, nil)
  1911  	c.Assert(err, IsNil)
  1912  	c.Assert(rw, NotNil)
  1913  
  1914  	err = rw.Update()
  1915  	c.Assert(err, Equals, gadget.ErrNoUpdate)
  1916  
  1917  	verifyDirContents(c, outDir, map[string]contentType{
  1918  		// / -> /
  1919  		"empty-dir":           typeDir,
  1920  		"non-empty/empty-dir": typeDir,
  1921  
  1922  		// / -> /foo
  1923  		"foo/empty-dir":           typeDir,
  1924  		"foo/non-empty/empty-dir": typeDir,
  1925  
  1926  		// /non-empty/empty-dir/ -> /contents-of-empty/
  1927  		"contents-of-empty": typeDir,
  1928  	})
  1929  }
  1930  
  1931  func (s *mountedfilesystemTestSuite) TestMountedUpdaterSameFileSkipped(c *C) {
  1932  	// some data for the gadget
  1933  	gd := []gadgetData{
  1934  		{name: "bar", target: "foo", content: "data"},
  1935  		{name: "bar", target: "some-dir/foo", content: "data"},
  1936  	}
  1937  	makeGadgetData(c, s.dir, gd)
  1938  
  1939  	outDir := filepath.Join(c.MkDir(), "out-dir")
  1940  	makeExistingData(c, outDir, []gadgetData{
  1941  		{target: "foo", content: "same"},
  1942  		{target: "some-dir/foo", content: "same"},
  1943  	})
  1944  
  1945  	ps := &gadget.LaidOutStructure{
  1946  		VolumeStructure: &gadget.VolumeStructure{
  1947  			Size:       2048,
  1948  			Filesystem: "ext4",
  1949  			Content: []gadget.VolumeContent{
  1950  				{
  1951  					UnresolvedSource: "bar",
  1952  					Target:           "/foo",
  1953  				}, {
  1954  					UnresolvedSource: "bar",
  1955  					Target:           "/some-dir/foo",
  1956  				},
  1957  			},
  1958  			Update: gadget.VolumeUpdate{
  1959  				Edition: 1,
  1960  			},
  1961  		},
  1962  	}
  1963  
  1964  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  1965  		c.Check(to, DeepEquals, ps)
  1966  		return outDir, nil
  1967  	}, nil)
  1968  	c.Assert(err, IsNil)
  1969  	c.Assert(rw, NotNil)
  1970  
  1971  	// pretend a backup pass ran and found the files identical
  1972  	makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.same"), 0, nil)
  1973  	makeSizedFile(c, filepath.Join(s.backup, "struct-0/some-dir/foo.same"), 0, nil)
  1974  
  1975  	err = rw.Update()
  1976  	c.Assert(err, Equals, gadget.ErrNoUpdate)
  1977  	// files were not modified
  1978  	verifyWrittenGadgetData(c, outDir, []gadgetData{
  1979  		{target: "foo", content: "same"},
  1980  		{target: "some-dir/foo", content: "same"},
  1981  	})
  1982  }
  1983  
  1984  func (s *mountedfilesystemTestSuite) TestMountedUpdaterLonePrefix(c *C) {
  1985  	// some data for the gadget
  1986  	gd := []gadgetData{
  1987  		{name: "bar", target: "1/nested/bar", content: "data"},
  1988  		{name: "bar", target: "2/nested/foo", content: "data"},
  1989  		{name: "bar", target: "3/nested/bar", content: "data"},
  1990  	}
  1991  	makeGadgetData(c, s.dir, gd)
  1992  
  1993  	outDir := filepath.Join(c.MkDir(), "out-dir")
  1994  
  1995  	ps := &gadget.LaidOutStructure{
  1996  		VolumeStructure: &gadget.VolumeStructure{
  1997  			Size:       2048,
  1998  			Filesystem: "ext4",
  1999  			Content: []gadget.VolumeContent{
  2000  				{
  2001  					UnresolvedSource: "bar",
  2002  					Target:           "/1/nested/",
  2003  				}, {
  2004  					UnresolvedSource: "bar",
  2005  					Target:           "/2/nested/foo",
  2006  				}, {
  2007  					UnresolvedSource: "/",
  2008  					Target:           "/3/nested/",
  2009  				},
  2010  			},
  2011  			Update: gadget.VolumeUpdate{
  2012  				Edition: 1,
  2013  			},
  2014  		},
  2015  	}
  2016  
  2017  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  2018  		c.Check(to, DeepEquals, ps)
  2019  		return outDir, nil
  2020  	}, nil)
  2021  	c.Assert(err, IsNil)
  2022  	c.Assert(rw, NotNil)
  2023  
  2024  	err = rw.Update()
  2025  	c.Assert(err, IsNil)
  2026  	verifyWrittenGadgetData(c, outDir, gd)
  2027  }
  2028  
  2029  func (s *mountedfilesystemTestSuite) TestMountedUpdaterUpdateErrorOnSymlinkToFile(c *C) {
  2030  	gdWritten := []gadgetData{
  2031  		{name: "data", target: "data", content: "some data"},
  2032  		{name: "foo", symlinkTo: "data"},
  2033  	}
  2034  	makeGadgetData(c, s.dir, gdWritten)
  2035  
  2036  	outDir := filepath.Join(c.MkDir(), "out-dir")
  2037  
  2038  	existing := []gadgetData{
  2039  		{target: "data", content: "some data"},
  2040  	}
  2041  	makeExistingData(c, outDir, existing)
  2042  
  2043  	ps := &gadget.LaidOutStructure{
  2044  		VolumeStructure: &gadget.VolumeStructure{
  2045  			Size:       2048,
  2046  			Filesystem: "ext4",
  2047  			Content: []gadget.VolumeContent{
  2048  				{UnresolvedSource: "/", Target: "/"},
  2049  			},
  2050  		},
  2051  	}
  2052  
  2053  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  2054  		c.Check(to, DeepEquals, ps)
  2055  		return outDir, nil
  2056  	}, nil)
  2057  	c.Assert(err, IsNil)
  2058  	c.Assert(rw, NotNil)
  2059  
  2060  	// create a mock backup of first file
  2061  	makeSizedFile(c, filepath.Join(s.backup, "struct-0/data.backup"), 0, nil)
  2062  
  2063  	err = rw.Update()
  2064  	c.Assert(err, ErrorMatches, "cannot update content: cannot update file /foo: symbolic links are not supported")
  2065  }
  2066  
  2067  func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupErrorOnSymlinkToDir(c *C) {
  2068  	gd := []gadgetData{
  2069  		{name: "bar/data", target: "bar/data", content: "some data"},
  2070  		{name: "baz", symlinkTo: "bar"},
  2071  	}
  2072  	makeGadgetData(c, s.dir, gd)
  2073  
  2074  	outDir := filepath.Join(c.MkDir(), "out-dir")
  2075  
  2076  	existing := []gadgetData{
  2077  		{target: "bar/data", content: "some data"},
  2078  	}
  2079  	makeExistingData(c, outDir, existing)
  2080  
  2081  	ps := &gadget.LaidOutStructure{
  2082  		VolumeStructure: &gadget.VolumeStructure{
  2083  			Size:       2048,
  2084  			Filesystem: "ext4",
  2085  			Content: []gadget.VolumeContent{
  2086  				{UnresolvedSource: "/", Target: "/"},
  2087  			},
  2088  		},
  2089  	}
  2090  
  2091  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  2092  		c.Check(to, DeepEquals, ps)
  2093  		return outDir, nil
  2094  	}, nil)
  2095  	c.Assert(err, IsNil)
  2096  	c.Assert(rw, NotNil)
  2097  
  2098  	// create a mock backup of first file
  2099  	makeSizedFile(c, filepath.Join(s.backup, "struct-0/bar/data.backup"), 0, nil)
  2100  	makeSizedFile(c, filepath.Join(s.backup, "struct-0/bar.backup"), 0, nil)
  2101  
  2102  	err = rw.Update()
  2103  	c.Assert(err, ErrorMatches, "cannot update content: cannot update file /baz: symbolic links are not supported")
  2104  }
  2105  
  2106  func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackFromBackup(c *C) {
  2107  	// some data for the gadget
  2108  	gd := []gadgetData{
  2109  		{name: "bar", target: "foo", content: "data"},
  2110  		{name: "bar", target: "some-dir/foo", content: "data"},
  2111  	}
  2112  	makeGadgetData(c, s.dir, gd)
  2113  
  2114  	outDir := filepath.Join(c.MkDir(), "out-dir")
  2115  	makeExistingData(c, outDir, []gadgetData{
  2116  		{target: "foo", content: "written"},
  2117  		{target: "some-dir/foo", content: "written"},
  2118  	})
  2119  
  2120  	ps := &gadget.LaidOutStructure{
  2121  		VolumeStructure: &gadget.VolumeStructure{
  2122  			Size:       2048,
  2123  			Filesystem: "ext4",
  2124  			Content: []gadget.VolumeContent{
  2125  				{
  2126  					UnresolvedSource: "bar",
  2127  					Target:           "/foo",
  2128  				}, {
  2129  					UnresolvedSource: "bar",
  2130  					Target:           "/some-dir/foo",
  2131  				},
  2132  			},
  2133  			Update: gadget.VolumeUpdate{
  2134  				Edition: 1,
  2135  			},
  2136  		},
  2137  	}
  2138  
  2139  	muo := &mockContentUpdateObserver{
  2140  		c:              c,
  2141  		expectedStruct: ps,
  2142  	}
  2143  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  2144  		c.Check(to, DeepEquals, ps)
  2145  		return outDir, nil
  2146  	}, muo)
  2147  	c.Assert(err, IsNil)
  2148  	c.Assert(rw, NotNil)
  2149  
  2150  	// pretend a backup pass ran and created a backup
  2151  	makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.backup"), 0, []byte("backup"))
  2152  	makeSizedFile(c, filepath.Join(s.backup, "struct-0/some-dir/foo.backup"), 0, []byte("backup"))
  2153  
  2154  	err = rw.Rollback()
  2155  	c.Assert(err, IsNil)
  2156  	// files were restored from backup
  2157  	verifyWrittenGadgetData(c, outDir, []gadgetData{
  2158  		{target: "foo", content: "backup"},
  2159  		{target: "some-dir/foo", content: "backup"},
  2160  	})
  2161  
  2162  	// only notified about content getting updated
  2163  	c.Check(muo.contentRollback, DeepEquals, map[string][]*mockContentChange{
  2164  		outDir: {
  2165  			// rollback restores from the backups
  2166  			{"foo", &gadget.ContentChange{
  2167  				After:  filepath.Join(s.dir, "bar"),
  2168  				Before: filepath.Join(s.backup, "struct-0/foo.backup"),
  2169  			}},
  2170  			{"some-dir/foo", &gadget.ContentChange{
  2171  				After:  filepath.Join(s.dir, "bar"),
  2172  				Before: filepath.Join(s.backup, "struct-0/some-dir/foo.backup"),
  2173  			}},
  2174  		},
  2175  	})
  2176  }
  2177  
  2178  func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackSkipSame(c *C) {
  2179  	// some data for the gadget
  2180  	gd := []gadgetData{
  2181  		{name: "bar", content: "data"},
  2182  	}
  2183  	makeGadgetData(c, s.dir, gd)
  2184  
  2185  	outDir := filepath.Join(c.MkDir(), "out-dir")
  2186  	makeExistingData(c, outDir, []gadgetData{
  2187  		{target: "foo", content: "same"},
  2188  	})
  2189  
  2190  	ps := &gadget.LaidOutStructure{
  2191  		VolumeStructure: &gadget.VolumeStructure{
  2192  			Size:       2048,
  2193  			Filesystem: "ext4",
  2194  			Content: []gadget.VolumeContent{
  2195  				{
  2196  					UnresolvedSource: "bar",
  2197  					Target:           "/foo",
  2198  				},
  2199  			},
  2200  			Update: gadget.VolumeUpdate{
  2201  				Edition: 1,
  2202  			},
  2203  		},
  2204  	}
  2205  
  2206  	muo := &mockContentUpdateObserver{
  2207  		c:              c,
  2208  		expectedStruct: ps,
  2209  	}
  2210  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  2211  		c.Check(to, DeepEquals, ps)
  2212  		return outDir, nil
  2213  	}, muo)
  2214  	c.Assert(err, IsNil)
  2215  	c.Assert(rw, NotNil)
  2216  
  2217  	// pretend a backup pass ran and created a backup
  2218  	makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.same"), 0, nil)
  2219  
  2220  	err = rw.Rollback()
  2221  	c.Assert(err, IsNil)
  2222  	// files were not modified
  2223  	verifyWrittenGadgetData(c, outDir, []gadgetData{
  2224  		{target: "foo", content: "same"},
  2225  	})
  2226  	// identical content did not need a rollback, no notifications
  2227  	c.Check(muo.contentRollback, HasLen, 0)
  2228  }
  2229  
  2230  func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackSkipPreserved(c *C) {
  2231  	// some data for the gadget
  2232  	gd := []gadgetData{
  2233  		{name: "bar", content: "data"},
  2234  	}
  2235  	makeGadgetData(c, s.dir, gd)
  2236  
  2237  	outDir := filepath.Join(c.MkDir(), "out-dir")
  2238  	makeExistingData(c, outDir, []gadgetData{
  2239  		{target: "foo", content: "preserved"},
  2240  	})
  2241  
  2242  	ps := &gadget.LaidOutStructure{
  2243  		VolumeStructure: &gadget.VolumeStructure{
  2244  			Size:       2048,
  2245  			Filesystem: "ext4",
  2246  			Content: []gadget.VolumeContent{
  2247  				{
  2248  					UnresolvedSource: "bar",
  2249  					Target:           "/foo",
  2250  				},
  2251  			},
  2252  			Update: gadget.VolumeUpdate{
  2253  				Edition:  1,
  2254  				Preserve: []string{"foo"},
  2255  			},
  2256  		},
  2257  	}
  2258  
  2259  	muo := &mockContentUpdateObserver{
  2260  		c:              c,
  2261  		expectedStruct: ps,
  2262  	}
  2263  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  2264  		c.Check(to, DeepEquals, ps)
  2265  		return outDir, nil
  2266  	}, muo)
  2267  	c.Assert(err, IsNil)
  2268  	c.Assert(rw, NotNil)
  2269  
  2270  	// preserved files get no backup, but gets a stamp instead
  2271  	makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.preserve"), 0, nil)
  2272  
  2273  	err = rw.Rollback()
  2274  	c.Assert(err, IsNil)
  2275  	// files were not modified
  2276  	verifyWrittenGadgetData(c, outDir, []gadgetData{
  2277  		{target: "foo", content: "preserved"},
  2278  	})
  2279  	// preserved content did not need a rollback, no notifications
  2280  	c.Check(muo.contentRollback, HasLen, 0)
  2281  }
  2282  
  2283  func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackNewFiles(c *C) {
  2284  	makeGadgetData(c, s.dir, []gadgetData{
  2285  		{name: "bar", content: "data"},
  2286  	})
  2287  
  2288  	outDir := filepath.Join(c.MkDir(), "out-dir")
  2289  	makeExistingData(c, outDir, []gadgetData{
  2290  		{target: "foo", content: "written"},
  2291  		{target: "some-dir/bar", content: "written"},
  2292  		{target: "this/is/some/deep/nesting/bar", content: "written"},
  2293  	})
  2294  
  2295  	ps := &gadget.LaidOutStructure{
  2296  		VolumeStructure: &gadget.VolumeStructure{
  2297  			Size:       2048,
  2298  			Filesystem: "ext4",
  2299  			Content: []gadget.VolumeContent{
  2300  				{
  2301  					UnresolvedSource: "bar",
  2302  					Target:           "/foo",
  2303  				}, {
  2304  					UnresolvedSource: "bar",
  2305  					Target:           "some-dir/",
  2306  				}, {
  2307  					UnresolvedSource: "bar",
  2308  					Target:           "/this/is/some/deep/nesting/",
  2309  				},
  2310  			},
  2311  			Update: gadget.VolumeUpdate{
  2312  				Edition: 1,
  2313  			},
  2314  		},
  2315  	}
  2316  
  2317  	muo := &mockContentUpdateObserver{
  2318  		c:              c,
  2319  		expectedStruct: ps,
  2320  	}
  2321  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  2322  		c.Check(to, DeepEquals, ps)
  2323  		return outDir, nil
  2324  	}, muo)
  2325  	c.Assert(err, IsNil)
  2326  	c.Assert(rw, NotNil)
  2327  
  2328  	// none of the marker files exists, files are new, will be removed
  2329  	err = rw.Rollback()
  2330  	c.Assert(err, IsNil)
  2331  	// everything was removed
  2332  	verifyDirContents(c, outDir, map[string]contentType{})
  2333  	// new files were rolled back
  2334  	c.Check(muo.contentRollback, DeepEquals, map[string][]*mockContentChange{
  2335  		outDir: {
  2336  			// files did not exist, so there was no 'before' content
  2337  			{"foo", &gadget.ContentChange{
  2338  				After:  filepath.Join(s.dir, "bar"),
  2339  				Before: "",
  2340  			}},
  2341  			{"some-dir/bar", &gadget.ContentChange{
  2342  				After:  filepath.Join(s.dir, "bar"),
  2343  				Before: "",
  2344  			}},
  2345  			{"this/is/some/deep/nesting/bar", &gadget.ContentChange{
  2346  				After:  filepath.Join(s.dir, "bar"),
  2347  				Before: "",
  2348  			}},
  2349  		},
  2350  	})
  2351  }
  2352  
  2353  func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackRestoreFails(c *C) {
  2354  	if os.Geteuid() == 0 {
  2355  		c.Skip("the test cannot be run by the root user")
  2356  	}
  2357  
  2358  	makeGadgetData(c, s.dir, []gadgetData{
  2359  		{name: "bar", content: "data"},
  2360  	})
  2361  
  2362  	outDir := filepath.Join(c.MkDir(), "out-dir")
  2363  	makeExistingData(c, outDir, []gadgetData{
  2364  		{target: "foo", content: "written"},
  2365  		{target: "some-dir/foo", content: "written"},
  2366  	})
  2367  	// the file exists, and cannot be modified directly, rollback will still
  2368  	// restore the backup as we atomically swap copies with rename()
  2369  	err := os.Chmod(filepath.Join(outDir, "foo"), 0000)
  2370  	c.Assert(err, IsNil)
  2371  
  2372  	ps := &gadget.LaidOutStructure{
  2373  		VolumeStructure: &gadget.VolumeStructure{
  2374  			Size:       2048,
  2375  			Filesystem: "ext4",
  2376  			Content: []gadget.VolumeContent{
  2377  				{
  2378  					UnresolvedSource: "bar",
  2379  					Target:           "/foo",
  2380  				}, {
  2381  					UnresolvedSource: "bar",
  2382  					Target:           "/some-dir/foo",
  2383  				},
  2384  			},
  2385  			Update: gadget.VolumeUpdate{
  2386  				Edition: 1,
  2387  			},
  2388  		},
  2389  	}
  2390  
  2391  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  2392  		c.Check(to, DeepEquals, ps)
  2393  		return outDir, nil
  2394  	}, nil)
  2395  	c.Assert(err, IsNil)
  2396  	c.Assert(rw, NotNil)
  2397  
  2398  	// one file backed up, the other is new
  2399  	makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.backup"), 0, []byte("backup"))
  2400  
  2401  	err = rw.Rollback()
  2402  	c.Assert(err, IsNil)
  2403  	// the file was restored
  2404  	c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "backup")
  2405  	// directory was removed
  2406  	c.Check(osutil.IsDirectory(filepath.Join(outDir, "some-dir")), Equals, false)
  2407  
  2408  	// mock the data again
  2409  	makeExistingData(c, outDir, []gadgetData{
  2410  		{target: "foo", content: "written"},
  2411  		{target: "some-dir/foo", content: "written"},
  2412  	})
  2413  
  2414  	// make the directory non-writable
  2415  	err = os.Chmod(filepath.Join(outDir, "some-dir"), 0555)
  2416  	c.Assert(err, IsNil)
  2417  	// restore permissions later, otherwise test suite cleanup complains
  2418  	defer os.Chmod(filepath.Join(outDir, "some-dir"), 0755)
  2419  
  2420  	err = rw.Rollback()
  2421  	c.Assert(err, ErrorMatches, "cannot rollback content: cannot remove written update: remove .*/out-dir/some-dir/foo: permission denied")
  2422  }
  2423  
  2424  func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackNotWritten(c *C) {
  2425  	makeGadgetData(c, s.dir, []gadgetData{
  2426  		{name: "bar", content: "data"},
  2427  	})
  2428  
  2429  	outDir := filepath.Join(c.MkDir(), "out-dir")
  2430  
  2431  	ps := &gadget.LaidOutStructure{
  2432  		VolumeStructure: &gadget.VolumeStructure{
  2433  			Size:       2048,
  2434  			Filesystem: "ext4",
  2435  			Content: []gadget.VolumeContent{
  2436  				{
  2437  					UnresolvedSource: "bar",
  2438  					Target:           "/foo",
  2439  				}, {
  2440  					UnresolvedSource: "bar",
  2441  					Target:           "/some-dir/foo",
  2442  				},
  2443  			},
  2444  			Update: gadget.VolumeUpdate{
  2445  				Edition: 1,
  2446  			},
  2447  		},
  2448  	}
  2449  
  2450  	muo := &mockContentUpdateObserver{
  2451  		c:              c,
  2452  		expectedStruct: ps,
  2453  	}
  2454  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  2455  		c.Check(to, DeepEquals, ps)
  2456  		return outDir, nil
  2457  	}, muo)
  2458  	c.Assert(err, IsNil)
  2459  	c.Assert(rw, NotNil)
  2460  
  2461  	// rollback does not error out if files were not written
  2462  	err = rw.Rollback()
  2463  	c.Assert(err, IsNil)
  2464  	// observer would be notified that files were to be written, and so it
  2465  	// must be notified when they would be rolled back
  2466  	c.Check(muo.contentRollback, DeepEquals, map[string][]*mockContentChange{
  2467  		outDir: {
  2468  			// rollback restores from the backups
  2469  			{"foo", &gadget.ContentChange{
  2470  				After: filepath.Join(s.dir, "bar"),
  2471  			}},
  2472  			{"some-dir/foo", &gadget.ContentChange{
  2473  				After: filepath.Join(s.dir, "bar"),
  2474  			}},
  2475  		},
  2476  	})
  2477  }
  2478  
  2479  func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackDirectory(c *C) {
  2480  	makeGadgetData(c, s.dir, []gadgetData{
  2481  		{name: "some-dir/bar", content: "data"},
  2482  		{name: "some-dir/foo", content: "data"},
  2483  		{name: "some-dir/nested/nested-foo", content: "data"},
  2484  		{name: "empty-dir/"},
  2485  		{name: "bar", content: "data"},
  2486  	})
  2487  
  2488  	outDir := filepath.Join(c.MkDir(), "out-dir")
  2489  	makeExistingData(c, outDir, []gadgetData{
  2490  		// some-dir/ -> /
  2491  		{target: "foo", content: "written"},
  2492  		{target: "bar", content: "written"},
  2493  		{target: "nested/nested-foo", content: "written"},
  2494  		// some-dir/ -> /other-dir/
  2495  		{target: "other-dir/foo", content: "written"},
  2496  		{target: "other-dir/bar", content: "written"},
  2497  		{target: "other-dir/nested/nested-foo", content: "written"},
  2498  		// some-dir/nested -> /other-dir/nested/
  2499  		{target: "other-dir/nested/nested/nested-foo", content: "written"},
  2500  		// bar -> /this/is/some/deep/nesting/
  2501  		{target: "this/is/some/deep/nesting/bar", content: "written"},
  2502  		{target: "lone-dir/"},
  2503  	})
  2504  
  2505  	ps := &gadget.LaidOutStructure{
  2506  		VolumeStructure: &gadget.VolumeStructure{
  2507  			Size:       2048,
  2508  			Filesystem: "ext4",
  2509  			Content: []gadget.VolumeContent{
  2510  				{
  2511  					UnresolvedSource: "some-dir/",
  2512  					Target:           "/",
  2513  				}, {
  2514  					UnresolvedSource: "some-dir/",
  2515  					Target:           "/other-dir/",
  2516  				}, {
  2517  					UnresolvedSource: "some-dir/nested",
  2518  					Target:           "/other-dir/nested/",
  2519  				}, {
  2520  					UnresolvedSource: "bar",
  2521  					Target:           "/this/is/some/deep/nesting/",
  2522  				}, {
  2523  					UnresolvedSource: "empty-dir/",
  2524  					Target:           "/lone-dir/",
  2525  				},
  2526  			},
  2527  			Update: gadget.VolumeUpdate{
  2528  				Edition: 1,
  2529  			},
  2530  		},
  2531  	}
  2532  
  2533  	muo := &mockContentUpdateObserver{
  2534  		c:              c,
  2535  		expectedStruct: ps,
  2536  	}
  2537  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  2538  		c.Check(to, DeepEquals, ps)
  2539  		return outDir, nil
  2540  	}, muo)
  2541  	c.Assert(err, IsNil)
  2542  	c.Assert(rw, NotNil)
  2543  
  2544  	// one file backed up
  2545  	makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.backup"), 0, []byte("backup"))
  2546  	// pretend part of the directory structure existed before
  2547  	makeSizedFile(c, filepath.Join(s.backup, "struct-0/this/is/some.backup"), 0, nil)
  2548  	makeSizedFile(c, filepath.Join(s.backup, "struct-0/this/is.backup"), 0, nil)
  2549  	makeSizedFile(c, filepath.Join(s.backup, "struct-0/this.backup"), 0, nil)
  2550  	makeSizedFile(c, filepath.Join(s.backup, "struct-0/lone-dir.backup"), 0, nil)
  2551  
  2552  	// files without a marker are new, will be removed
  2553  	err = rw.Rollback()
  2554  	c.Assert(err, IsNil)
  2555  
  2556  	verifyDirContents(c, outDir, map[string]contentType{
  2557  		"lone-dir":     typeDir,
  2558  		"this/is/some": typeDir,
  2559  		"foo":          typeFile,
  2560  	})
  2561  	// this one got restored
  2562  	c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "backup")
  2563  
  2564  	c.Check(muo.contentRollback, DeepEquals, map[string][]*mockContentChange{
  2565  		outDir: {
  2566  			// a new file
  2567  			{"bar", &gadget.ContentChange{
  2568  				After: filepath.Join(s.dir, "some-dir/bar"),
  2569  			}},
  2570  			// this file was restored from backup
  2571  			{"foo", &gadget.ContentChange{
  2572  				After:  filepath.Join(s.dir, "some-dir/foo"),
  2573  				Before: filepath.Join(s.backup, "struct-0/foo.backup"),
  2574  			}},
  2575  			// new files till the end
  2576  			{"nested/nested-foo", &gadget.ContentChange{
  2577  				After: filepath.Join(s.dir, "some-dir/nested/nested-foo"),
  2578  			}},
  2579  			{"other-dir/bar", &gadget.ContentChange{
  2580  				After: filepath.Join(s.dir, "some-dir/bar"),
  2581  			}},
  2582  			{"other-dir/foo", &gadget.ContentChange{
  2583  				After: filepath.Join(s.dir, "some-dir/foo"),
  2584  			}},
  2585  			{"other-dir/nested/nested-foo", &gadget.ContentChange{
  2586  				After: filepath.Join(s.dir, "some-dir/nested/nested-foo"),
  2587  			}},
  2588  			{"other-dir/nested/nested/nested-foo", &gadget.ContentChange{
  2589  				After: filepath.Join(s.dir, "some-dir/nested/nested-foo"),
  2590  			}},
  2591  			{"this/is/some/deep/nesting/bar", &gadget.ContentChange{
  2592  				After: filepath.Join(s.dir, "bar"),
  2593  			}},
  2594  		},
  2595  	})
  2596  }
  2597  
  2598  func (s *mountedfilesystemTestSuite) TestMountedUpdaterEndToEndOne(c *C) {
  2599  	// some data for the gadget
  2600  	gdWritten := []gadgetData{
  2601  		{name: "foo", target: "foo-dir/foo", content: "data"},
  2602  		{name: "bar", target: "bar-name", content: "data"},
  2603  		{name: "boot-assets/splash", target: "splash", content: "data"},
  2604  		{name: "boot-assets/dtb", target: "dtb", content: "data"},
  2605  		{name: "boot-assets/some-dir/data", target: "some-dir/data", content: "data"},
  2606  		{name: "boot-assets/some-dir/empty-file", target: "some-dir/empty-file", content: ""},
  2607  		{name: "boot-assets/nested-dir/more-nested/more", target: "/nested-copy/more-nested/more", content: "data"},
  2608  	}
  2609  	gdNotWritten := []gadgetData{
  2610  		{name: "foo", target: "/foo", content: "data"},
  2611  		{name: "boot-assets/some-dir/data", target: "data-copy", content: "data"},
  2612  		{name: "boot-assets/nested-dir/nested", target: "/nested-copy/nested", content: "data"},
  2613  		{name: "preserved/same-content", target: "preserved/same-content", content: "can't touch this"},
  2614  	}
  2615  	gdSameContent := []gadgetData{
  2616  		{name: "foo", target: "/foo-same", content: "data"},
  2617  	}
  2618  	makeGadgetData(c, s.dir, append(gdWritten, gdNotWritten...))
  2619  	err := os.MkdirAll(filepath.Join(s.dir, "boot-assets/empty-dir"), 0755)
  2620  	c.Assert(err, IsNil)
  2621  
  2622  	outDir := filepath.Join(c.MkDir(), "out-dir")
  2623  
  2624  	makeExistingData(c, outDir, []gadgetData{
  2625  		{target: "dtb", content: "updated"},
  2626  		{target: "foo", content: "can't touch this"},
  2627  		{target: "foo-same", content: "data"},
  2628  		{target: "data-copy-preserved", content: "can't touch this"},
  2629  		{target: "data-copy", content: "can't touch this"},
  2630  		{target: "nested-copy/nested", content: "can't touch this"},
  2631  		{target: "nested-copy/more-nested/"},
  2632  		{target: "not-listed", content: "can't touch this"},
  2633  		{target: "unrelated/data/here", content: "unrelated"},
  2634  		{target: "preserved/same-content-for-list", content: "can't touch this"},
  2635  		{target: "preserved/same-content-for-observer", content: "can't touch this"},
  2636  	})
  2637  	// these exist in the root directory and are preserved
  2638  	preserve := []string{
  2639  		// mix entries with leading / and without
  2640  		"/foo",
  2641  		"/data-copy-preserved",
  2642  		"nested-copy/nested",
  2643  		"not-listed", // not present in 'gadget' contents
  2644  		"preserved/same-content-for-list",
  2645  	}
  2646  	// these are preserved, but don't exist in the root, so data from gadget
  2647  	// will be written
  2648  	preserveButNotPresent := []string{
  2649  		"/bar-name",
  2650  		"some-dir/data",
  2651  	}
  2652  
  2653  	ps := &gadget.LaidOutStructure{
  2654  		VolumeStructure: &gadget.VolumeStructure{
  2655  			Size:       2048,
  2656  			Filesystem: "ext4",
  2657  			Content: []gadget.VolumeContent{
  2658  				{
  2659  					UnresolvedSource: "foo",
  2660  					Target:           "/foo-dir/",
  2661  				}, {
  2662  					// would overwrite /foo
  2663  					UnresolvedSource: "foo",
  2664  					Target:           "/",
  2665  				}, {
  2666  					// nothing written, content is unchanged
  2667  					UnresolvedSource: "foo",
  2668  					Target:           "/foo-same",
  2669  				}, {
  2670  					// preserved, but not present, will be
  2671  					// written
  2672  					UnresolvedSource: "bar",
  2673  					Target:           "/bar-name",
  2674  				}, {
  2675  					// some-dir/data is preserved, but not
  2676  					// present, hence will be written
  2677  					UnresolvedSource: "boot-assets/",
  2678  					Target:           "/",
  2679  				}, {
  2680  					// would overwrite /data-copy
  2681  					UnresolvedSource: "boot-assets/some-dir/data",
  2682  					Target:           "/data-copy-preserved",
  2683  				}, {
  2684  					UnresolvedSource: "boot-assets/some-dir/data",
  2685  					Target:           "/data-copy",
  2686  				}, {
  2687  					// would overwrite /nested-copy/nested
  2688  					UnresolvedSource: "boot-assets/nested-dir/",
  2689  					Target:           "/nested-copy/",
  2690  				}, {
  2691  					UnresolvedSource: "boot-assets",
  2692  					Target:           "/boot-assets-copy/",
  2693  				}, {
  2694  					UnresolvedSource: "/boot-assets/empty-dir/",
  2695  					Target:           "/lone-dir/nested/",
  2696  				}, {
  2697  					UnresolvedSource: "preserved/same-content",
  2698  					Target:           "preserved/same-content-for-list",
  2699  				}, {
  2700  					UnresolvedSource: "preserved/same-content",
  2701  					Target:           "preserved/same-content-for-observer",
  2702  				},
  2703  			},
  2704  			Update: gadget.VolumeUpdate{
  2705  				Edition:  1,
  2706  				Preserve: append(preserve, preserveButNotPresent...),
  2707  			},
  2708  		},
  2709  	}
  2710  
  2711  	muo := &mockContentUpdateObserver{
  2712  		c:              c,
  2713  		expectedStruct: ps,
  2714  		preserveTargets: []string{
  2715  			"preserved/same-content-for-observer",
  2716  		},
  2717  	}
  2718  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  2719  		c.Check(to, DeepEquals, ps)
  2720  		return outDir, nil
  2721  	}, muo)
  2722  	c.Assert(err, IsNil)
  2723  	c.Assert(rw, NotNil)
  2724  
  2725  	originalState := map[string]contentType{
  2726  		"foo":                                 typeFile,
  2727  		"foo-same":                            typeFile,
  2728  		"dtb":                                 typeFile,
  2729  		"data-copy":                           typeFile,
  2730  		"not-listed":                          typeFile,
  2731  		"data-copy-preserved":                 typeFile,
  2732  		"nested-copy/nested":                  typeFile,
  2733  		"nested-copy/more-nested":             typeDir,
  2734  		"unrelated/data/here":                 typeFile,
  2735  		"preserved/same-content-for-list":     typeFile,
  2736  		"preserved/same-content-for-observer": typeFile,
  2737  	}
  2738  	verifyDirContents(c, outDir, originalState)
  2739  
  2740  	// run the backup phase
  2741  	err = rw.Backup()
  2742  	c.Assert(err, IsNil)
  2743  
  2744  	verifyDirContents(c, filepath.Join(s.backup, "struct-0"), map[string]contentType{
  2745  		"nested-copy.backup":                       typeFile,
  2746  		"nested-copy/nested.preserve":              typeFile,
  2747  		"nested-copy/more-nested.backup":           typeFile,
  2748  		"foo.preserve":                             typeFile,
  2749  		"foo-same.same":                            typeFile,
  2750  		"data-copy-preserved.preserve":             typeFile,
  2751  		"data-copy.backup":                         typeFile,
  2752  		"dtb.backup":                               typeFile,
  2753  		"preserved.backup":                         typeFile,
  2754  		"preserved/same-content-for-list.preserve": typeFile,
  2755  		"preserved/same-content-for-observer.same": typeFile,
  2756  	})
  2757  
  2758  	expectedObservedContentChange := map[string][]*mockContentChange{
  2759  		// observer is notified about changed and new files
  2760  		outDir: {
  2761  			{"foo-dir/foo", &gadget.ContentChange{
  2762  				After: filepath.Join(s.dir, "foo"),
  2763  			}},
  2764  			{"bar-name", &gadget.ContentChange{
  2765  				After: filepath.Join(s.dir, "bar"),
  2766  			}},
  2767  			// update with changed content
  2768  			{"dtb", &gadget.ContentChange{
  2769  				After:  filepath.Join(s.dir, "boot-assets/dtb"),
  2770  				Before: filepath.Join(s.backup, "struct-0/dtb.backup"),
  2771  			}},
  2772  			// new files
  2773  			{"nested-dir/more-nested/more", &gadget.ContentChange{
  2774  				After: filepath.Join(s.dir, "boot-assets/nested-dir/more-nested/more"),
  2775  			}},
  2776  			{"nested-dir/nested", &gadget.ContentChange{
  2777  				After: filepath.Join(s.dir, "boot-assets/nested-dir/nested"),
  2778  			}},
  2779  			{"some-dir/data", &gadget.ContentChange{
  2780  				After: filepath.Join(s.dir, "boot-assets/some-dir/data"),
  2781  			}},
  2782  			// new files
  2783  			{"some-dir/empty-file", &gadget.ContentChange{
  2784  				After: filepath.Join(s.dir, "boot-assets/some-dir/empty-file"),
  2785  			}},
  2786  			{"splash", &gadget.ContentChange{
  2787  				After: filepath.Join(s.dir, "boot-assets/splash"),
  2788  			}},
  2789  			// update with changed content
  2790  			{"data-copy", &gadget.ContentChange{
  2791  				After:  filepath.Join(s.dir, "boot-assets/some-dir/data"),
  2792  				Before: filepath.Join(s.backup, "struct-0/data-copy.backup"),
  2793  			}},
  2794  			// new files
  2795  			{"nested-copy/more-nested/more", &gadget.ContentChange{
  2796  				After: filepath.Join(s.dir, "boot-assets/nested-dir/more-nested/more"),
  2797  			}},
  2798  			{"boot-assets-copy/boot-assets/dtb", &gadget.ContentChange{
  2799  				After: filepath.Join(s.dir, "boot-assets/dtb"),
  2800  			}},
  2801  			{"boot-assets-copy/boot-assets/nested-dir/more-nested/more", &gadget.ContentChange{
  2802  				After: filepath.Join(s.dir, "boot-assets/nested-dir/more-nested/more")},
  2803  			},
  2804  			{"boot-assets-copy/boot-assets/nested-dir/nested", &gadget.ContentChange{
  2805  				After: filepath.Join(s.dir, "boot-assets/nested-dir/nested"),
  2806  			}},
  2807  			{"boot-assets-copy/boot-assets/some-dir/data", &gadget.ContentChange{
  2808  				After: filepath.Join(s.dir, "boot-assets/some-dir/data"),
  2809  			}},
  2810  			{"boot-assets-copy/boot-assets/some-dir/empty-file", &gadget.ContentChange{
  2811  				After: filepath.Join(s.dir, "boot-assets/some-dir/empty-file"),
  2812  			}},
  2813  			{"boot-assets-copy/boot-assets/splash", &gadget.ContentChange{
  2814  				After: filepath.Join(s.dir, "boot-assets/splash"),
  2815  			}},
  2816  		},
  2817  	}
  2818  
  2819  	// observe calls happen in the order the structure content gets analyzed
  2820  	c.Check(muo.contentUpdate, DeepEquals, expectedObservedContentChange)
  2821  
  2822  	// run the update phase
  2823  	err = rw.Update()
  2824  	c.Assert(err, IsNil)
  2825  
  2826  	verifyDirContents(c, outDir, map[string]contentType{
  2827  		"foo":        typeFile,
  2828  		"foo-same":   typeFile,
  2829  		"not-listed": typeFile,
  2830  
  2831  		// boot-assets/some-dir/data -> /data-copy
  2832  		"data-copy": typeFile,
  2833  
  2834  		// boot-assets/some-dir/data -> /data-copy-preserved
  2835  		"data-copy-preserved": typeFile,
  2836  
  2837  		// foo -> /foo-dir/
  2838  		"foo-dir/foo": typeFile,
  2839  
  2840  		// bar -> /bar-name
  2841  		"bar-name": typeFile,
  2842  
  2843  		// boot-assets/ -> /
  2844  		"dtb":                         typeFile,
  2845  		"splash":                      typeFile,
  2846  		"some-dir/data":               typeFile,
  2847  		"some-dir/empty-file":         typeFile,
  2848  		"nested-dir/nested":           typeFile,
  2849  		"nested-dir/more-nested/more": typeFile,
  2850  		"empty-dir":                   typeDir,
  2851  
  2852  		// boot-assets -> /boot-assets-copy/
  2853  		"boot-assets-copy/boot-assets/dtb":                         typeFile,
  2854  		"boot-assets-copy/boot-assets/splash":                      typeFile,
  2855  		"boot-assets-copy/boot-assets/some-dir/data":               typeFile,
  2856  		"boot-assets-copy/boot-assets/some-dir/empty-file":         typeFile,
  2857  		"boot-assets-copy/boot-assets/nested-dir/nested":           typeFile,
  2858  		"boot-assets-copy/boot-assets/nested-dir/more-nested/more": typeFile,
  2859  		"boot-assets-copy/boot-assets/empty-dir":                   typeDir,
  2860  
  2861  		// boot-assets/nested-dir/ -> /nested-copy/
  2862  		"nested-copy/nested":           typeFile,
  2863  		"nested-copy/more-nested/more": typeFile,
  2864  
  2865  		// data that was not part of the update
  2866  		"unrelated/data/here": typeFile,
  2867  
  2868  		// boot-assets/empty-dir/ -> /lone-dir/nested/
  2869  		"lone-dir/nested": typeDir,
  2870  
  2871  		"preserved/same-content-for-list":     typeFile,
  2872  		"preserved/same-content-for-observer": typeFile,
  2873  	})
  2874  
  2875  	// files that existed were preserved
  2876  	for _, en := range preserve {
  2877  		p := filepath.Join(outDir, en)
  2878  		c.Check(p, testutil.FileEquals, "can't touch this")
  2879  	}
  2880  	// everything else was written
  2881  	verifyWrittenGadgetData(c, outDir, append(gdWritten, gdSameContent...))
  2882  
  2883  	err = rw.Rollback()
  2884  	c.Assert(err, IsNil)
  2885  	// back to square one
  2886  	verifyDirContents(c, outDir, originalState)
  2887  
  2888  	c.Check(muo.contentRollback, DeepEquals, expectedObservedContentChange)
  2889  	// call rollback once more, we should observe the same files again
  2890  	muo.contentRollback = nil
  2891  	err = rw.Rollback()
  2892  	c.Assert(err, IsNil)
  2893  	c.Check(muo.contentRollback, DeepEquals, expectedObservedContentChange)
  2894  	// file contents are unchanged
  2895  	verifyDirContents(c, outDir, originalState)
  2896  }
  2897  
  2898  func (s *mountedfilesystemTestSuite) TestMountedUpdaterTrivialValidation(c *C) {
  2899  	psNoFs := &gadget.LaidOutStructure{
  2900  		VolumeStructure: &gadget.VolumeStructure{
  2901  			Size: 2048,
  2902  			// no filesystem
  2903  			Content: []gadget.VolumeContent{},
  2904  		},
  2905  	}
  2906  
  2907  	lookupFail := func(to *gadget.LaidOutStructure) (string, error) {
  2908  		c.Fatalf("unexpected call")
  2909  		return "", nil
  2910  	}
  2911  
  2912  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, psNoFs, s.backup, lookupFail, nil)
  2913  	c.Assert(err, ErrorMatches, "structure #0 has no filesystem")
  2914  	c.Assert(rw, IsNil)
  2915  
  2916  	ps := &gadget.LaidOutStructure{
  2917  		VolumeStructure: &gadget.VolumeStructure{
  2918  			Size:       2048,
  2919  			Filesystem: "ext4",
  2920  			Content:    []gadget.VolumeContent{},
  2921  		},
  2922  	}
  2923  
  2924  	rw, err = gadget.NewMountedFilesystemUpdater("", ps, s.backup, lookupFail, nil)
  2925  	c.Assert(err, ErrorMatches, `internal error: gadget content directory cannot be unset`)
  2926  	c.Assert(rw, IsNil)
  2927  
  2928  	rw, err = gadget.NewMountedFilesystemUpdater(s.dir, ps, "", lookupFail, nil)
  2929  	c.Assert(err, ErrorMatches, `internal error: backup directory must not be unset`)
  2930  	c.Assert(rw, IsNil)
  2931  
  2932  	rw, err = gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, nil, nil)
  2933  	c.Assert(err, ErrorMatches, `internal error: mount lookup helper must be provided`)
  2934  	c.Assert(rw, IsNil)
  2935  
  2936  	rw, err = gadget.NewMountedFilesystemUpdater(s.dir, nil, s.backup, lookupFail, nil)
  2937  	c.Assert(err, ErrorMatches, `internal error: \*LaidOutStructure.*`)
  2938  	c.Assert(rw, IsNil)
  2939  
  2940  	lookupOk := func(to *gadget.LaidOutStructure) (string, error) {
  2941  		return filepath.Join(s.dir, "foobar"), nil
  2942  	}
  2943  
  2944  	for _, tc := range []struct {
  2945  		content gadget.VolumeContent
  2946  		match   string
  2947  	}{
  2948  		{content: gadget.VolumeContent{UnresolvedSource: "", Target: "/"}, match: "internal error: source cannot be unset"},
  2949  		{content: gadget.VolumeContent{UnresolvedSource: "/", Target: ""}, match: "internal error: target cannot be unset"},
  2950  	} {
  2951  		testPs := &gadget.LaidOutStructure{
  2952  			VolumeStructure: &gadget.VolumeStructure{
  2953  				Size:       2048,
  2954  				Filesystem: "ext4",
  2955  				Content:    []gadget.VolumeContent{tc.content},
  2956  			},
  2957  		}
  2958  
  2959  		rw, err := gadget.NewMountedFilesystemUpdater(s.dir, testPs, s.backup, lookupOk, nil)
  2960  		c.Assert(err, IsNil)
  2961  		c.Assert(rw, NotNil)
  2962  
  2963  		err = rw.Update()
  2964  		c.Assert(err, ErrorMatches, "cannot update content: "+tc.match)
  2965  
  2966  		err = rw.Backup()
  2967  		c.Assert(err, ErrorMatches, "cannot backup content: "+tc.match)
  2968  
  2969  		err = rw.Rollback()
  2970  		c.Assert(err, ErrorMatches, "cannot rollback content: "+tc.match)
  2971  	}
  2972  }
  2973  
  2974  func (s *mountedfilesystemTestSuite) TestMountedUpdaterMountLookupFail(c *C) {
  2975  	ps := &gadget.LaidOutStructure{
  2976  		VolumeStructure: &gadget.VolumeStructure{
  2977  			Size:       2048,
  2978  			Filesystem: "ext4",
  2979  			Content: []gadget.VolumeContent{
  2980  				{UnresolvedSource: "/", Target: "/"},
  2981  			},
  2982  			Update: gadget.VolumeUpdate{
  2983  				Edition: 1,
  2984  			},
  2985  		},
  2986  	}
  2987  
  2988  	lookupFail := func(to *gadget.LaidOutStructure) (string, error) {
  2989  		c.Check(to, DeepEquals, ps)
  2990  		return "", errors.New("fail fail fail")
  2991  	}
  2992  
  2993  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, lookupFail, nil)
  2994  	c.Assert(err, ErrorMatches, "cannot find mount location of structure #0: fail fail fail")
  2995  	c.Assert(rw, IsNil)
  2996  }
  2997  
  2998  func (s *mountedfilesystemTestSuite) TestMountedUpdaterNonFilePreserveError(c *C) {
  2999  	// some data for the gadget
  3000  	gd := []gadgetData{
  3001  		{name: "foo", content: "data"},
  3002  	}
  3003  	makeGadgetData(c, s.dir, gd)
  3004  
  3005  	outDir := filepath.Join(c.MkDir(), "out-dir")
  3006  	// will conflict with preserve entry
  3007  	err := os.MkdirAll(filepath.Join(outDir, "foo"), 0755)
  3008  	c.Assert(err, IsNil)
  3009  
  3010  	ps := &gadget.LaidOutStructure{
  3011  		VolumeStructure: &gadget.VolumeStructure{
  3012  			Size:       2048,
  3013  			Filesystem: "ext4",
  3014  			Content: []gadget.VolumeContent{
  3015  				{UnresolvedSource: "/", Target: "/"},
  3016  			},
  3017  			Update: gadget.VolumeUpdate{
  3018  				Preserve: []string{"foo"},
  3019  				Edition:  1,
  3020  			},
  3021  		},
  3022  	}
  3023  
  3024  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
  3025  		c.Check(to, DeepEquals, ps)
  3026  		return outDir, nil
  3027  	}, nil)
  3028  	c.Assert(err, IsNil)
  3029  	c.Assert(rw, NotNil)
  3030  
  3031  	err = rw.Backup()
  3032  	c.Check(err, ErrorMatches, `cannot map preserve entries for mount location ".*/out-dir": preserved entry "foo" cannot be a directory`)
  3033  	err = rw.Update()
  3034  	c.Check(err, ErrorMatches, `cannot map preserve entries for mount location ".*/out-dir": preserved entry "foo" cannot be a directory`)
  3035  	err = rw.Rollback()
  3036  	c.Check(err, ErrorMatches, `cannot map preserve entries for mount location ".*/out-dir": preserved entry "foo" cannot be a directory`)
  3037  }
  3038  
  3039  func (s *mountedfilesystemTestSuite) TestMountedUpdaterObserverPreservesBootAssets(c *C) {
  3040  	// mirror pc-amd64-gadget
  3041  	gd := []gadgetData{
  3042  		{name: "grub.conf", content: "grub.conf from gadget"},
  3043  		{name: "grubx64.efi", content: "grubx64.efi from gadget"},
  3044  		{name: "foo", content: "foo from gadget"},
  3045  	}
  3046  	makeGadgetData(c, s.dir, gd)
  3047  
  3048  	outDir := filepath.Join(c.MkDir(), "out-dir")
  3049  
  3050  	existingGrubCfg := `# Snapd-Boot-Config-Edition: 1
  3051  managed grub.cfg from disk`
  3052  	makeExistingData(c, outDir, []gadgetData{
  3053  		{target: "EFI/boot/grubx64.efi", content: "grubx64.efi from disk"},
  3054  		{target: "EFI/ubuntu/grub.cfg", content: existingGrubCfg},
  3055  		{target: "foo", content: "foo from disk"},
  3056  	})
  3057  	// based on pc gadget
  3058  	ps := &gadget.LaidOutStructure{
  3059  		VolumeStructure: &gadget.VolumeStructure{
  3060  			Size:       2048,
  3061  			Role:       gadget.SystemBoot,
  3062  			Filesystem: "ext4",
  3063  			Content: []gadget.VolumeContent{
  3064  				{UnresolvedSource: "grubx64.efi", Target: "EFI/boot/grubx64.efi"},
  3065  				{UnresolvedSource: "grub.conf", Target: "EFI/ubuntu/grub.cfg"},
  3066  				{UnresolvedSource: "foo", Target: "foo"},
  3067  			},
  3068  			Update: gadget.VolumeUpdate{
  3069  				Preserve: []string{"foo"},
  3070  				Edition:  1,
  3071  			},
  3072  		},
  3073  	}
  3074  	obs := &mockContentUpdateObserver{
  3075  		c:               c,
  3076  		expectedStruct:  ps,
  3077  		preserveTargets: []string{"EFI/ubuntu/grub.cfg"},
  3078  	}
  3079  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup,
  3080  		func(to *gadget.LaidOutStructure) (string, error) {
  3081  			c.Check(to, DeepEquals, ps)
  3082  			return outDir, nil
  3083  		},
  3084  		obs)
  3085  	c.Assert(err, IsNil)
  3086  	c.Assert(rw, NotNil)
  3087  
  3088  	expectedFileStamps := map[string]contentType{
  3089  		"EFI.backup":                  typeFile,
  3090  		"EFI/boot/grubx64.efi.backup": typeFile,
  3091  		"EFI/boot.backup":             typeFile,
  3092  		"EFI/ubuntu.backup":           typeFile,
  3093  
  3094  		// listed explicitly in the structure
  3095  		"foo.preserve": typeFile,
  3096  		// requested by observer
  3097  		"EFI/ubuntu/grub.cfg.ignore": typeFile,
  3098  	}
  3099  
  3100  	for _, step := range []struct {
  3101  		name string
  3102  		call func() error
  3103  	}{
  3104  		{name: "backup", call: rw.Backup},
  3105  		{name: "update", call: rw.Update},
  3106  		{name: "rollback", call: rw.Rollback},
  3107  	} {
  3108  		c.Logf("step: %v", step.name)
  3109  		err := step.call()
  3110  		c.Assert(err, IsNil)
  3111  
  3112  		switch step.name {
  3113  		case "backup":
  3114  			c.Check(filepath.Join(outDir, "EFI/boot/grubx64.efi"), testutil.FileEquals, "grubx64.efi from disk")
  3115  			c.Check(filepath.Join(outDir, "EFI/ubuntu/grub.cfg"), testutil.FileEquals, existingGrubCfg)
  3116  			c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "foo from disk")
  3117  		case "update":
  3118  			c.Check(filepath.Join(outDir, "EFI/boot/grubx64.efi"), testutil.FileEquals, "grubx64.efi from gadget")
  3119  			c.Check(filepath.Join(outDir, "EFI/ubuntu/grub.cfg"), testutil.FileEquals,
  3120  				`# Snapd-Boot-Config-Edition: 1
  3121  managed grub.cfg from disk`)
  3122  			c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "foo from disk")
  3123  		case "rollback":
  3124  			c.Check(filepath.Join(outDir, "EFI/boot/grubx64.efi"), testutil.FileEquals, "grubx64.efi from disk")
  3125  			c.Check(filepath.Join(outDir, "EFI/ubuntu/grub.cfg"), testutil.FileEquals, existingGrubCfg)
  3126  			c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "foo from disk")
  3127  		default:
  3128  			c.Fatalf("unexpected step: %q", step.name)
  3129  		}
  3130  		verifyDirContents(c, filepath.Join(s.backup, "struct-0"), expectedFileStamps)
  3131  	}
  3132  }
  3133  
  3134  var (
  3135  	// based on pc gadget
  3136  	psForObserver = &gadget.LaidOutStructure{
  3137  		VolumeStructure: &gadget.VolumeStructure{
  3138  			Size:       2048,
  3139  			Role:       gadget.SystemBoot,
  3140  			Filesystem: "ext4",
  3141  			Content: []gadget.VolumeContent{
  3142  				{UnresolvedSource: "foo", Target: "foo"},
  3143  			},
  3144  			Update: gadget.VolumeUpdate{
  3145  				Edition: 1,
  3146  			},
  3147  		},
  3148  	}
  3149  )
  3150  
  3151  func (s *mountedfilesystemTestSuite) TestMountedUpdaterObserverPreserveNewFile(c *C) {
  3152  	gd := []gadgetData{
  3153  		{name: "foo", content: "foo from gadget"},
  3154  	}
  3155  	makeGadgetData(c, s.dir, gd)
  3156  
  3157  	outDir := filepath.Join(c.MkDir(), "out-dir")
  3158  
  3159  	obs := &mockContentUpdateObserver{
  3160  		c:               c,
  3161  		expectedStruct:  psForObserver,
  3162  		preserveTargets: []string{"foo"},
  3163  	}
  3164  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, psForObserver, s.backup,
  3165  		func(to *gadget.LaidOutStructure) (string, error) {
  3166  			c.Check(to, DeepEquals, psForObserver)
  3167  			return outDir, nil
  3168  		},
  3169  		obs)
  3170  	c.Assert(err, IsNil)
  3171  	c.Assert(rw, NotNil)
  3172  
  3173  	expectedNewFileChanges := map[string][]*mockContentChange{
  3174  		outDir: {
  3175  			{"foo", &gadget.ContentChange{After: filepath.Join(s.dir, "foo")}},
  3176  		},
  3177  	}
  3178  	expectedStamps := map[string]contentType{
  3179  		"foo.ignore": typeFile,
  3180  	}
  3181  	// file does not exist
  3182  	err = rw.Backup()
  3183  	c.Assert(err, IsNil)
  3184  	// no stamps
  3185  	verifyDirContents(c, filepath.Join(s.backup, "struct-0"), expectedStamps)
  3186  	// observer got notified about change
  3187  	c.Assert(obs.contentUpdate, DeepEquals, expectedNewFileChanges)
  3188  
  3189  	obs.reset()
  3190  
  3191  	// try the same pass again
  3192  	err = rw.Backup()
  3193  	c.Assert(err, IsNil)
  3194  	verifyDirContents(c, filepath.Join(s.backup, "struct-0"), expectedStamps)
  3195  	// observer already requested the change to be ignored once
  3196  	c.Assert(obs.contentUpdate, HasLen, 0)
  3197  
  3198  	// file does not exist and is not written
  3199  	err = rw.Update()
  3200  	c.Assert(err, Equals, gadget.ErrNoUpdate)
  3201  	c.Assert(filepath.Join(outDir, "foo"), testutil.FileAbsent)
  3202  
  3203  	// nothing happens on rollback
  3204  	err = rw.Rollback()
  3205  	c.Assert(err, IsNil)
  3206  	c.Assert(filepath.Join(outDir, "foo"), testutil.FileAbsent)
  3207  }
  3208  
  3209  func (s *mountedfilesystemTestSuite) TestMountedUpdaterObserverPreserveExistingFile(c *C) {
  3210  	gd := []gadgetData{
  3211  		{name: "foo", content: "foo from gadget"},
  3212  	}
  3213  	makeGadgetData(c, s.dir, gd)
  3214  
  3215  	outDir := filepath.Join(c.MkDir(), "out-dir")
  3216  
  3217  	obs := &mockContentUpdateObserver{
  3218  		c:               c,
  3219  		expectedStruct:  psForObserver,
  3220  		preserveTargets: []string{"foo"},
  3221  	}
  3222  	rw, err := gadget.NewMountedFilesystemUpdater(s.dir, psForObserver, s.backup,
  3223  		func(to *gadget.LaidOutStructure) (string, error) {
  3224  			c.Check(to, DeepEquals, psForObserver)
  3225  			return outDir, nil
  3226  		},
  3227  		obs)
  3228  	c.Assert(err, IsNil)
  3229  	c.Assert(rw, NotNil)
  3230  
  3231  	// file exists now
  3232  	makeExistingData(c, outDir, []gadgetData{
  3233  		{target: "foo", content: "foo from disk"},
  3234  	})
  3235  	expectedExistingFileChanges := map[string][]*mockContentChange{
  3236  		outDir: {
  3237  			{"foo", &gadget.ContentChange{
  3238  				After:  filepath.Join(s.dir, "foo"),
  3239  				Before: filepath.Join(s.backup, "struct-0/foo.backup"),
  3240  			}},
  3241  		},
  3242  	}
  3243  	expectedExistingFileStamps := map[string]contentType{
  3244  		"foo.ignore": typeFile,
  3245  	}
  3246  	err = rw.Backup()
  3247  	c.Assert(err, IsNil)
  3248  	verifyDirContents(c, filepath.Join(s.backup, "struct-0"), expectedExistingFileStamps)
  3249  	// get notified about change
  3250  	c.Assert(obs.contentUpdate, DeepEquals, expectedExistingFileChanges)
  3251  
  3252  	obs.reset()
  3253  	// backup called again (eg. after reset)
  3254  	err = rw.Backup()
  3255  	c.Assert(err, IsNil)
  3256  	verifyDirContents(c, filepath.Join(s.backup, "struct-0"), expectedExistingFileStamps)
  3257  	// observer already requested the change to be ignored once
  3258  	c.Assert(obs.contentUpdate, HasLen, 0)
  3259  
  3260  	// and nothing gets updated
  3261  	err = rw.Update()
  3262  	c.Assert(err, Equals, gadget.ErrNoUpdate)
  3263  	c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "foo from disk")
  3264  
  3265  	// the file existed and was preserved, nothing gets removed on rollback
  3266  	err = rw.Rollback()
  3267  	c.Assert(err, IsNil)
  3268  	c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "foo from disk")
  3269  }