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