github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/gadget/mountedfilesystem_test.go (about)

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