Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gadget, osutil: use atomic file copy, adjust tests #9103

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 3 additions & 5 deletions gadget/mountedfilesystem.go
Expand Up @@ -217,12 +217,10 @@ func writeFileOrSymlink(src, dst string, preserveInDst []string) error {
return fmt.Errorf("cannot write a symlink: %v", err)
}
} else {
// overwrite & sync by default
copyFlags := osutil.CopyFlagOverwrite | osutil.CopyFlagSync

// TODO use osutil.AtomicFile
// TODO try to preserve ownership and permission bits
if err := osutil.CopyFile(src, dst, copyFlags); err != nil {

// dst is a reflection of src, no additional flags are needed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand this comment - but maybe I just need more tea?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite sure it needs a better comment, which I'll add

if err := osutil.CopyFileAtomic(src, dst, 0); err != nil {
return fmt.Errorf("cannot copy %s: %v", src, err)
}
}
Expand Down
20 changes: 14 additions & 6 deletions gadget/mountedfilesystem_test.go
Expand Up @@ -473,7 +473,7 @@ func (s *mountedfilesystemTestSuite) TestMountedWriterConflictingDestinationDire

// can't overwrite a directory with a file
err = rw.Write(outDir, nil)
c.Assert(err, ErrorMatches, fmt.Sprintf("cannot write filesystem content of source:foo-dir: cannot copy .*: unable to create %s/foo-dir: .* is a directory", outDir))
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))

}

Expand Down Expand Up @@ -2021,7 +2021,8 @@ func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackRestoreFails(c *C
{target: "foo", content: "written"},
{target: "some-dir/foo", content: "written"},
})
// make rollback fail when restoring
// the file exists, rollback will try to restore it, but make the file
// non-modifiable directly
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this updated comment, but even though the file is 0000 rollback will restore it, yes? Maybe the comment could mention this too?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add.

err := os.Chmod(filepath.Join(outDir, "foo"), 0000)
c.Assert(err, IsNil)

Expand Down Expand Up @@ -2055,12 +2056,19 @@ func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackRestoreFails(c *C
makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.backup"), 0, []byte("backup"))

err = rw.Rollback()
c.Assert(err, ErrorMatches, "cannot rollback content: cannot copy .*: unable to create .*/out-dir/foo: permission denied")
c.Assert(err, IsNil)
// the file was restored
c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "backup")
// directory was removed
c.Check(osutil.IsDirectory(filepath.Join(outDir, "some-dir")), Equals, false)

// remove offending file
c.Assert(os.Remove(filepath.Join(outDir, "foo")), IsNil)
// mock the data again
makeExistingData(c, outDir, []gadgetData{
{target: "foo", content: "written"},
{target: "some-dir/foo", content: "written"},
})

// make destination dir non-writable
// make the directory non-writable
err = os.Chmod(filepath.Join(outDir, "some-dir"), 0555)
c.Assert(err, IsNil)
// restore permissions later, otherwise test suite cleanup complains
Expand Down
40 changes: 40 additions & 0 deletions osutil/cp.go
Expand Up @@ -117,6 +117,46 @@ func CopyFile(src, dst string, flags CopyFlag) (err error) {
return nil
}

// CopyFileAtomic copies src to dst using AtomicFile internally to create the
// destination. The destination path is always overwritten. The destination and
// the owning directory are synced after copy completes. Pass additional flags
// for AtomicFile wrapping the destination.
func CopyFileAtomic(src, dst string, flags AtomicWriteFlags) (err error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It slightly bothers me slightly that "CopyFileAtomic" takes different flags than "CopyFile()". OTOH maybe the perfect should not be the enemy of the good and we can always tweak/fix further as needed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that we cannot really make any use of the flags. CopyFlagSync is meaningless because we always sync, CopyFlagOverwrite is racy unless we try to open the dst file with O_CREAT|O_EXCL next to AtomicFile, lastly CopyFlagPreserveAll cannot be implemented right now without calling cp under the hood. In that sense, the semantics are more like AtomicFile, so instead passing CopyFlags, it's taking AtomicWriteFlags.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this should be called AtomicCopyFile. Regarding needeing cp, we could use it if needed and cp to a random file name and then AtomicRename that to the final name, it's not our current use case I think though.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other possible naming would be AtomicWriteFileCopy, in that case though src and dst would have to be swapped I think

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like AtomicWriteFileCopy() and we can always consolidate into CopyFile() with the Atomic flag later, maybe a XXX: should we merge into CopyFIle with new AtomicCopyFile or similar.

fin, err := openfile(src, os.O_RDONLY, 0)
if err != nil {
return fmt.Errorf("unable to open source file %s: %v", src, err)
}
defer func() {
if cerr := fin.Close(); cerr != nil && err == nil {
err = fmt.Errorf("when closing %s: %v", src, cerr)
}
}()

fi, err := fin.Stat()
if err != nil {
return fmt.Errorf("unable to stat %s: %v", src, err)
}

fout, err := NewAtomicFile(dst, fi.Mode(), flags, NoChown, NoChown)
if err != nil {
return fmt.Errorf("cannot create atomic file: %v", err)
}
defer func() {
if cerr := fout.Cancel(); cerr != ErrCannotCancel && err == nil {
err = fmt.Errorf("cannot cancel temporary file copy %s: %v", fout.Name(), cerr)
}
}()

if err := copyfile(fin, fout, fi); err != nil {
return fmt.Errorf("unable to copy %s to %s: %v", src, fout.Name(), err)
}

if err := fout.Commit(); err != nil {
return fmt.Errorf("cannot commit atomic file copy: %v", err)
}
return nil
}

func runCmd(cmd *exec.Cmd, errdesc string) error {
if output, err := cmd.CombinedOutput(); err != nil {
output = bytes.TrimSpace(output)
Expand Down
71 changes: 71 additions & 0 deletions osutil/cp_test.go
Expand Up @@ -300,3 +300,74 @@ func (s *cpSuite) TestCopyPreserveAllSyncSyncFailure(c *C) {
{"sync"},
})
}

func (s *cpSuite) TestCopyAtomicSimple(c *C) {
err := CopyFileAtomic(s.f1, s.f2, 0)
c.Assert(err, IsNil)
c.Assert(s.f2, testutil.FileEquals, s.data)

}

func (s *cpSuite) TestCopyAtomicOverwrites(c *C) {
err := ioutil.WriteFile(s.f2, []byte("this is f2 content"), 0644)
c.Assert(err, IsNil)

err = CopyFileAtomic(s.f1, s.f2, 0)
c.Assert(err, IsNil)
c.Assert(s.f2, testutil.FileEquals, s.data)
}

func (s *cpSuite) TestCopyAtomicSymlinks(c *C) {
f2Symlink := filepath.Join(s.dir, "f2-symlink")
err := os.Symlink(s.f2, f2Symlink)
c.Assert(err, IsNil)

f2SymlinkNoFollow := filepath.Join(s.dir, "f2-symlink-no-follow")
err = os.Symlink(s.f2, f2SymlinkNoFollow)
c.Assert(err, IsNil)

// follows symlink, dst is f2
err = CopyFileAtomic(s.f1, f2Symlink, AtomicWriteFollow)
c.Assert(err, IsNil)
c.Check(IsSymlink(f2Symlink), Equals, true, Commentf("%q is not a symlink", f2Symlink))
c.Check(s.f2, testutil.FileEquals, s.data)
c.Check(f2SymlinkNoFollow, testutil.FileEquals, s.data)

// when not following, copy overwrites the symlink
err = CopyFileAtomic(s.f1, f2SymlinkNoFollow, 0)
c.Assert(err, IsNil)
c.Check(IsSymlink(f2SymlinkNoFollow), Equals, false, Commentf("%q is not a file", f2SymlinkNoFollow))
c.Check(f2SymlinkNoFollow, testutil.FileEquals, s.data)
}

func (s *cpSuite) TestCopyAtomicErrReal(c *C) {
err := CopyFileAtomic(filepath.Join(s.dir, "random-file"), s.f2, 0)
c.Assert(err, ErrorMatches, "unable to open source file .*/random-file: open .* no such file or directory")

dir := c.MkDir()

err = CopyFileAtomic(s.f1, filepath.Join(dir, "random-dir", "f3"), 0)
c.Assert(err, ErrorMatches, `cannot create atomic file: open .*/random-dir/f3\.[a-zA-Z0-9]+~: no such file or directory`)

err = os.MkdirAll(filepath.Join(dir, "read-only"), 0000)
c.Assert(err, IsNil)
err = CopyFileAtomic(s.f1, filepath.Join(dir, "read-only", "f3"), 0)
c.Assert(err, ErrorMatches, `cannot create atomic file: open .*/read-only/f3\.[a-zA-Z0-9]+~: permission denied`)
}

func (s *cpSuite) TestCopyAtomicErrMockedCopy(c *C) {
s.mock()
s.errs = []error{
nil, // openFile
nil, // src.Stat()
errors.New("copy fail"),
}

err := CopyFileAtomic(s.f1, s.f2, 0)
c.Assert(err, ErrorMatches, `unable to copy .*/f1 to .*/f2\.[a-zA-Z0-9]+~: copy fail`)
entries, err := filepath.Glob(filepath.Join(s.dir, "*"))
c.Assert(err, IsNil)
c.Assert(entries, DeepEquals, []string{
filepath.Join(s.dir, "f1"),
})
}