osutil: add EnsureDirState and FileState #658

Merged
merged 37 commits into from Mar 17, 2016

Conversation

Projects
None yet
5 participants
Contributor

zyga commented Mar 14, 2016

This branch adds the EnsureDirState() function -- a flexible concept for maintaining a
subset of files in a directory. The function ensures that all the files
in the directory are exactly as they are expected to be and reports on
the changes made.

SyncDir() can be used to manage on-disk apparmor, seccomp, udev and
dbus configuration files. Reports about changes made can be used to
trigger udev configuration reload, load or unload apparmor profiles to
the kernel.

Signed-off-by: Zygmunt Krynicki zygmunt.krynicki@canonical.com

zyga added some commits Mar 14, 2016

interfaces/janitor: add janitor module
This patch adds the janitor -- a flexible concept for maintaining a
subset of files in a directory. The janitor ensures that all the files
in the directory are exactly as they are expected to be and reports on
the changes made.

The janitor can be used to manage on-disk apparmor, seccomp, udev and
dbus configuration files. Reports about changes made can be used to
trigger udev configuration reload, load or unload apparmor profiles to
the kernel.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
interaces/janitor: abbreviate test data
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
interaces/janitor: abbreviate more test data
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
interfaces/janitor/janitor.go
+// changes performed so far. Information about the performed changes is
+// returned to the caller for any extra processing that might be required (e.g.
+// to run some helper program).
+func (j *Janitor) Tidy(oracle map[string]*File) (removed, created, fixed []string, err error) {
@niemeyer

niemeyer Mar 14, 2016

Contributor

The abstraction is quite nice, but Janitor, Tidy, Oracle.. these are cool terms but not very helpful in understanding what's going on.

I suggest moving all of this functionality into a single function into the osutil package:

func SyncDir(dir, glob string, content map[string]*FileState) (created, corrected, removed []string, err error)
@niemeyer

niemeyer Mar 14, 2016

Contributor

Note the rename of fixed => corrected. "Fixed" also means "not moving", which is confusing in this context as the first two terms imply "moving".

@niemeyer

niemeyer Mar 14, 2016

Contributor

One more note: changed the order of parameters to be slightly more expected (what we want first, then what we don't want).

interfaces/janitor/janitor.go
+type File struct {
+ Content []byte
+ Mode os.FileMode
+ UID, Gid uint32
@niemeyer

niemeyer Mar 14, 2016

Contributor

UID and GID or Uid and Gid.

@zyga

zyga Mar 14, 2016

Contributor

golint complains and wants UID but Gid :-(

@niemeyer

niemeyer Mar 14, 2016

Contributor

The point of a linter is to make the code nicer. If it's doing the opposite, we need to either fix it or remove it.

Contributor

zyga commented Mar 14, 2016

Thanks for the review @niemeyer I'll get right to it.

interfaces/janitor/janitor.go
+ UID, Gid uint32
+}
+
+// Tidy the directory to match expectations.
@niemeyer

niemeyer Mar 14, 2016

Contributor

Per the earlier note, this should be a slightly more down-to-earth explanation.

The description should also cover what the keys in the map means.

@niemeyer niemeyer added the Reviewed label Mar 14, 2016

zyga added some commits Mar 14, 2016

interfaces/jantior,osutil: move janitor to osutil package
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: rename "fixed" to "corrected"
Fixed might also imply that a file has a "fixed location" and corrected
is unambiguous.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: reorder janitor return values
The new order is: created, corrected, removed

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: rename osutil.File to .FileState
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
+ *
+ */
+
+package osutil
@mvo5

mvo5 Mar 14, 2016

Collaborator

I'm feeling a bit uneasy that osutil becomes the new "helpers", i.e. collecting functionality that is only very loosely related. Maybe its own package?

@niemeyer

niemeyer Mar 14, 2016

Contributor

osutil is still growing indeed, but under a common theme.. What I suggest we do at some point is to split half of it into fsutil

osutil/sync_dir.go
+//
+// Note that Janitor doesn't use any filesystem observation APIs. All changes
+// are monitored on demand.
+type Janitor struct {
@mvo5

mvo5 Mar 14, 2016

Collaborator

I would prefer a less generic name than "Janitor" here, especially since it is a generic package like osutil. I don't have a great suggestion myself, maybe DirectoryEnforcer, DirContentEnforcer or somesuch.

@mvo5

mvo5 Mar 14, 2016

Collaborator

I just noticed that Gustavo commented earlier and suggested to move it here. Its fine (I think) however I would prefer more specific names/functions (EnforceDirectory() or SyncDirectory (but its more like a force instead of a sync but I won't argue over this, just my personal feeling).

osutil/sync_dir.go
+}
+
+// File describes the content and-meta data of one file managed by the janitor.
+type File struct {
@mvo5

mvo5 Mar 14, 2016

Collaborator

Same concern as above, this is in osutil but File is very generic, I'm concerned that we use a osutil.File for something that looks like its mostly tailored to the Janitor.

@zyga

zyga Mar 14, 2016

Contributor

This was renamed to FileState

zyga added some commits Mar 14, 2016

osutil: discard janitor as a concept, rename to SyncDir
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: improve documentation of FileState
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: use GID instead of Gid
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>

@zyga zyga removed the Reviewed label Mar 14, 2016

@zyga zyga changed the title from interfaces/janitor: add janitor module to osutil: add SyncDir() and FileState Mar 14, 2016

osutil/sync_dir.go
+
+// FileState describes the expected content and meta data of a single file.
+//
+// FileState is used by the SyncDir()
@niemeyer

niemeyer Mar 14, 2016

Contributor

We don't need to mention functions that use the given type. This is used everywhere it is used.

osutil/sync_dir.go
+//
+// FileState is used by the SyncDir()
+type FileState struct {
+ // Content is the entire content that a specific file is expected to have.
@niemeyer

niemeyer Mar 14, 2016

Contributor

All of that documentation is extremely boring and unnecessary, and it's not the first time we go through that.

Can we get go lint to stop complaining about such cases?

osutil/sync_dir.go
+// SyncDir enumerates all the files in the specified directory that match the
+// provided pattern (glob). Each enumerated file is checked to ensure that the
+// contents, permissions and ownership are what is desired. Unexpected files
+// are removed. Missing files are created and corrupted files are corrected.
@niemeyer

niemeyer Mar 14, 2016

Contributor

s/corrupted/differing/

osutil/sync_dir.go
+ baseName := path.Base(name)
+ var file *os.File
+ if file, err = os.OpenFile(name, os.O_RDWR, 0); err != nil {
+ return
@niemeyer

niemeyer Mar 14, 2016

Contributor

It's much better and safer to have all err returns being explicitly made, rather than having to instantiate every single value returned ahead of time, and being careful to not use := by mistake. Even if you get all of these right today, the next person (which may be you and me) will come here and miss those details, creating very unfortunate and hard to debug issues.

osutil/sync_dir.go
+ corrected = append(corrected, baseName)
+ }
+ found[baseName] = true
+ } else {
@niemeyer

niemeyer Mar 14, 2016

Contributor

Rather than having such long-spanning branches and having to carefully look back to identify which conditional this is talking about, it's much better to shortcut this quickly by saying:

data, ok := content[base]
if !ok {
    err := os.RemoveAll(name)
    ...
    continue
}

... continue unintended ...
osutil/sync_dir.go
+ if expected, shouldBeHere := content[baseName]; shouldBeHere {
+ changed := false
+ // Check that file has the right content
+ if stat.Size() == int64(len(expected.Content)) {
@niemeyer

niemeyer Mar 14, 2016

Contributor

Too much content for the loop. The number of indentations is a good hint.. the filesystem related ops inside this branch is another one. This is a very nice self-contained problem: write the file out.

osutil/sync_dir.go
+ removed = append(removed, baseName)
+ }
+ }
+ // Create files that were not found but are expected
@niemeyer

niemeyer Mar 14, 2016

Contributor

I suspect the code will end up cleaner if you do that first: create all content desired, and then remove all content undesired. The delta between creating something on top of existing or anew is smaller.

osutil/sync_dir.go
+ changed = true
+ }
+ } else {
+ if err = file.Truncate(0); err != nil {
@niemeyer

niemeyer Mar 14, 2016

Contributor

Hmm.. why are we doing this non-atomically? Is there a particular system that depends on this behavior?

@zyga

zyga Mar 14, 2016

Contributor

No, good catch!

osutil/sync_dir.go
+ for _, name := range matches {
+ baseName := path.Base(name)
+ var file *os.File
+ if file, err = os.OpenFile(name, os.O_RDWR, 0); err != nil {
@niemeyer

niemeyer Mar 14, 2016

Contributor

Again, a bit concerned about the atomicity here. Doesn't feel good to create a file in the right place with bad content and bad file mode.

@zyga

zyga Mar 14, 2016

Contributor

FYI, AFAIK we are not creating any files here, just opening existing files (we don't pass O_CREAT. I'll use AtomicWriteFile() elsewhere but I think the reading parts are correct.

@niemeyer niemeyer added the Reviewed label Mar 14, 2016

osutil/sync_dir.go
+ GID uint32
+}
+
+// SyncDir ensures that directory content matches expectations.
@niemeyer

niemeyer Mar 14, 2016

Contributor

@mvo5 suggested EnsureDirState to avoid the rsync feeling of DirSync.. sounds good.

zyga added some commits Mar 14, 2016

osutil: discard some obvious documentation
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: refer to differing content, not corrupted content
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: rename SyncDir() to EnsureDirState()
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: don't use implicit returns as those are tricky
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: move erase logic first so that longer path is less nested
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: refactor file writing code to private function
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: use AtomicWriteFile in EnsureDirState()
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: don't open a file if we just intend to remove it
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: tweak logic for more clarity
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: shorten error case and use AtomicWriteFile()
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil/sync_dir.go
+// to run some helper program).
+//
+// The return value is: created, corrected, removed []string, err error
+func EnsureDirState(dir, glob string, content map[string]*FileState) ([]string, []string, []string, error) {
@niemeyer

niemeyer Mar 15, 2016

Contributor

It's better to have the result value names in their place, instead of repeating the exact same data in the doc.

@niemeyer

niemeyer Mar 15, 2016

Contributor

Please note the underlying pattern: if the type is self-descriptive, we don't need a name to understand what it means. If the type is generic (string), it's better with the result names.

osutil/sync_dir.go
+ // Remove files that should not be here.
+ var expected *FileState
+ var shouldBeHere bool
+ if expected, shouldBeHere = content[baseName]; !shouldBeHere {
@niemeyer

niemeyer Mar 15, 2016

Contributor
foo, bar := baz[blah]
if !bar {

Rather than

var foo
var bar
if foo, bar = baz[blah]; !bar {
@zyga

zyga Mar 15, 2016

Contributor

I think this goes back to golint giving us exactly the opposite advice. Personally I find it tedious that we have to make golint happy this way when it flies in the face of what is more natural. I don't quite know how to convince golint to do better or start the process to evict it from having a say in our mergability.

osutil/sync_dir.go
+ removed = append(removed, baseName)
+ continue
+ }
+ var file *os.File
@niemeyer

niemeyer Mar 15, 2016

Contributor

Same..

osutil/sync_dir.go
+ return created, corrected, removed, err
+ }
+ var matched bool
+ if matched, err = filepath.Match(glob, baseName); err != nil {
@niemeyer

niemeyer Mar 15, 2016

Contributor

Same..

osutil/sync_dir.go
+ // Create files that were not found but are expected
+ for baseName, expected := range content {
+ if baseName != path.Base(baseName) {
+ err := fmt.Errorf("expected files cannot have path component: %q", baseName)
@niemeyer

niemeyer Mar 15, 2016

Contributor

This is a serious bug.. the call site broke the contract of the function, which means it cannot possibly work. I suggest panicking for the time being:

panic(fmt.Sprintf("EnsureDirState got a filename which is actually a file path: %q", baseName))
osutil/sync_dir.go
+ return created, corrected, removed, err
+ }
+ if !matched {
+ err := fmt.Errorf("expected files must match pattern: %q (pattern: %q)", baseName, glob)
@niemeyer

niemeyer Mar 15, 2016

Contributor
panic(fmt.Sprintf("EnsureDirState got filename %q which doesn't match the glob pattern %q", baseName, glob))
@chipaca

chipaca Mar 15, 2016

Member

or logger.Panicf(...) :-)

osutil/sync_dir.go
+ return created, corrected, removed, err
+ }
+ corrected = append(corrected, baseName)
+ // NOTE: rewriting files also fixes permissions so we can skip the last stage
@niemeyer

niemeyer Mar 15, 2016

Contributor

Where's ownership being handled?

@zyga

zyga Mar 15, 2016

Contributor

Good catch, it isn't after the re-arrange! I'll correct this.

osutil/sync_dir.go
+//
+// The return value is: created, corrected, removed []string, err error
+func EnsureDirState(dir, glob string, content map[string]*FileState) ([]string, []string, []string, error) {
+ matches, err := filepath.Glob(path.Join(dir, glob))
@niemeyer

niemeyer Mar 15, 2016

Contributor

These are file operations, so filepath rather than path everywhere.

@zyga

zyga Mar 15, 2016

Contributor

Thanks for pointing this out, I wasn't aware of the difference!

Contributor

niemeyer commented Mar 15, 2016

@zyga I still feel like the logic in this branch could be refactored to be harder to get wrong. So, a couple of concrete suggestions.

The first one is that I don't yet see a meaningful distinction between "this file already existed and was incorrect" and "this file did not exist". In both of these situations the file was bogus, and needs to be reloaded, so we may as well return both cases in a single changed list.

Then, the second one is based on a point I've made in the prior review. It's much easier to get logic right with isolated functions that are trying to do less.

Here is a candidate way to organize the function:

var errSameState = fmt.Errorf("file state has not changed")
...
for baseName, fileState := range content {
        filePath := filepath.Join(dir, baseName)
        err := writeFile(filePath, fileState)
        if err == errSameState {
                continue
        }
        if err != nil {
                return nil, nil, err
        }
        changed = append(changed, baseName)
}
for _, filePath := range matches {
        baseName := filepath.Base(filePath)
        if content[baseName] != nil {
                continue
        }
        err := os.Remove(filePath)
        if err != nil {
                return nil, nil, err
        }
        removed = append(removed, baseName) 
}

Contributor

niemeyer commented Mar 15, 2016

(improved sample with a minor tweak)

osutil/sync_dir_test.go
+ "github.com/ubuntu-core/snappy/osutil"
+)
+
+type JanitorSuite struct {
@mvo5

mvo5 Mar 15, 2016

Collaborator

Tiny nitpitck, maybe we can rename it to EnsureFileStateSuite or similar?

Contributor

zyga commented Mar 15, 2016

Thanks for the review everyone. I'll pick up where we left off yesterday and iterate.

zyga added some commits Mar 15, 2016

osutil: put return values in the function declaration
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: use filepath rather than path to join file names
Thanks to Gustavo for pointing out the difference.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: tweak code layout for being more natural
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: more tweaks to code layout
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: and more tweaks so that we don't declare variables
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: verify file ownership in all cases
Thanks to Gustavo for spotting the error in the logic flow.

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil/sync_dir.go
+ }
+ changed = true
+ }
+ if st, ok := stat.Sys().(*syscall.Stat_t); ok {
@zyga

zyga Mar 15, 2016

Contributor

This has a suble bug that I've introduced after switching to atomic writes. stat and file are not longer related to the newly-renamed file that was created by AtomicWrite()

zyga added some commits Mar 15, 2016

osutil: turn incorrect usage of EnsureDirState into panics
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: update test suite name after other renames
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>

@mvo5 mvo5 changed the title from osutil: add SyncDir() and FileState to osutil: add EnsureDirState() and FileState Mar 15, 2016

osutil: rewrite EnsureDirState() with fewer features
This patch substantially simplifies the aforementioned function by
removing UID/GID handling and by supporting just two levels of
distinction between changes made (files removed and files changed).

Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
+// The content map describes each of the files that are intended to exist in
+// the directory. Map keys must be file names relative to the directory.
+// Sub-directories in the name are not allowed.
+//
@jdstrand

jdstrand Mar 15, 2016

Contributor

"Sub-directories in the name are not allowed."

Where is this enforced? Should we have a test for this?

@niemeyer

niemeyer Mar 15, 2016

Contributor

Yes, let's have a validation pass on the input (both content and glob) before we even start processing anything.

Let's panic if there's invalid data there.. it'd mean we not only broke the contract of the function, but we've used extremely dangerous data by mistake.

@jdstrand

jdstrand Mar 17, 2016

Contributor

"Yes, let's have a validation pass on the input (both content and glob) before we even start processing anything.

Let's panic if there's invalid data there.. it'd mean we not only broke the contract of the function, but we've used extremely dangerous data by mistake."

This isn't implemented yet.

@jdstrand

jdstrand Mar 17, 2016

Contributor

Actually I see the sub-directory validation was implemented somewhere else. Ignore my previous comment.

+// Sub-directories in the name are not allowed.
+//
+// The function stops at the first encountered error but reports all of the
+// changes performed so far. Information about the performed changes is
@jdstrand

jdstrand Mar 15, 2016

Contributor

Perhaps be explicit here that the caller needs to handle when an error occurs since only some of the changes will have occurred.

@niemeyer

niemeyer Mar 15, 2016

Contributor

This is a good point. It'll be hard to manage such errors cases in a useful way. I'd prefer to go all the way to the opposite end, and into the safety side as far as security is concerned:

  • Define var firstErr error to track the first error we observe. Then:
  • On a write error
    • Track in firstErr
    • Set content = nil
    • Break out of the write loop so removals are attempted on every known file.
  • On a remove error
    • Track in firstErr
    • Continue
  • If firstErr != nil { return nil, nil, firstErr }
@niemeyer

niemeyer Mar 15, 2016

Contributor

(minor fix on the last line: return firstErr, not err)

@jdstrand

jdstrand Mar 15, 2016

Contributor

"

  • On a write error
    • Track in firstErr
    • Set content = nil
    • Break out of the write loop so removals are attempted on every known file.
      "

I like where you are going here since it seems EnsureDirState has everything it needs to do something right, but if file 1 is ok, file 2 has a write error and file 3 would be ok, file 1 is still applied, but files 2 and 3 are deleted (assuming I understand what you are suggesting). It almost seems better to go all through the loop, tracking all the errors so that 1 and 3 are processed and 2 is deleted. This is admittedly more complicated and I'm not sure how this function is intended to be used in the grand scheme of things, so may not be appropriate.

@niemeyer

niemeyer Mar 15, 2016

Contributor

That's not what I meant, and it sounds very dangerous to do that.

What I meant is all or nothing. On any errors, we bail out and remove all known content. This is the only sane thing to do here, IMO, because nobody has ever pondered about the consequences of partially applying an arbitrary subset of security profiles. It would be basically closing our eyes and hoping for the best, which is not a sane strategy with security.

By backing out and removing all known content we ensure that either what we asked for is accomplished, or nothing else is.

@jdstrand

jdstrand Mar 17, 2016

Contributor

EnsureDirState has the potential to operate on a full directory and indeed, that was how it was initially envisioned. However, we are now saying that EnsureDirState() should only be used per application. That suggests to me:

  • the name should change, perhaps to EnsureAppState
  • we should do input verification on the glob so it must match an app name regex
  • we should update the comments for the functions for the above two points accordingly
@niemeyer

niemeyer Mar 17, 2016

Contributor
  • the name should change, perhaps to EnsureAppState

No, this is a helper function. It's not per app, or per snap, or per anything really. The call site is what defines what it is. As such, the name is currently fine.

  • we should do input verification on the glob so it must match an app name regex

Again, separation of concerns. This is a helper which doesn't know about snaps. It's the call site that will define what the glob is.

@jdstrand

jdstrand Mar 17, 2016

Contributor

I actually meant to say basename here instead of glob.

@jdstrand

jdstrand Mar 17, 2016

Contributor

That said, with this function so general without a concrete example in the documentation at the top of the function, I'm finding it difficult to follow. Ie, I know what we are trying to achieve (I think! :), and so I am looking at how this function can fail, etc with bad arguments, but with things being generalized I see lots of place where a bad invocation can cause issues. Put another way-- we want to implement a specific thing for keeping files in sync, and we know what we want to use it for, but we are making it general for use cases that I don't know about.

If you want to keep it this way, fine, but I think it worth noting that I found it harder to follow and prove to myself that it was correct than I feel it should be.

@niemeyer

niemeyer Mar 17, 2016

Contributor

Understood, and sorry for making your life harder here. It'd have been easier for you to review this after the call site is in place. We can do another round once we have that.

+ stat, err := os.Stat(name)
+ c.Assert(err, IsNil)
+ c.Assert(stat.Mode().Perm(), Equals, os.FileMode(0600))
+}
@jdstrand

jdstrand Mar 15, 2016

Contributor

Since EnsureDirState is a loop, I think it would be wise to add a few tests for when the map is > 1. Eg: 2 files with no error, 1st file with error and 2nd ok, 1st file ok and 2nd error, etc.

@jdstrand

jdstrand Mar 15, 2016

Contributor

Another test to add would be one where the size/contents and perms are both different.

osutil/sync_dir.go
+
+func writeFile(filePath string, fileState *FileState) error {
+ stat, err := os.Stat(filePath)
+ if err != nil {
@niemeyer

niemeyer Mar 15, 2016

Contributor

This needs to be a more strict test:

if os.IsNotExist(err) {
        ... write file ...
}
if err != nil {
        return err
}
@jdstrand

jdstrand Mar 15, 2016

Contributor

This error check says that if there is an error, go ahead and write the file, but this only makes sense if the error is for the basename of the filePath does not exist. What if another path component doesn't exist? What if the error is EPERM (which can happen with more than just DAC perms). 'man fstatat' gives a lot of possible failure conditions. Looking at https://golang.org/src/os/file_unix.go?s=4060:4099#L150 it appears go itself is not being very helpful here, simply returning PathError if anything goes wrong.

@niemeyer

niemeyer Mar 15, 2016

Contributor

Beat you to it. ;)

+ // Return a special error if the file doesn't need to be changed
+ return errSameState
+ }
+ }
@jdstrand

jdstrand Mar 15, 2016

Contributor

Ownership and xattrs are not included here and I understand that is intentional. There should be a comment for EnsureDirState that calls these out explicitly so that developers are aware.

Xattrs aren't something that we currently use in snappy, but it is possible in the longer term future that we will incorporate them if/when AppArmor supports storing information there. It seems fine to not implement xattr syncing for now and to simply add it if/when needed (but again, please call this out in a comment for EnsureDirState).

Lack of ownership checks seems like a bug waiting to happen though-- I realize EnsureDirState currently only will run under a root-owned process, but it seems plausible that since this is a library function it will be asked to look at a directory with non-root owned files and this will clobber ownership. As mentioned, this should be mentioned in the comments for EnsureDirState. Supporting ownership syncs in a future commit seems wise though.

@zyga

zyga Mar 16, 2016

Contributor

I've added a comment about unsupported features.

File ownership was there earlier but we don't need this and cannot easily write test code for it so I'd rather just not check or support this yet. Xattrs fall under the same umbrella. I agree that in principle it would be nice to support it but we'd never use this feature in current codebase.

+ stat, err := os.Stat(name)
+ c.Assert(err, IsNil)
+ c.Assert(stat.Mode().Perm(), Equals, os.FileMode(0600))
+}
@jdstrand

jdstrand Mar 15, 2016

Contributor

Minor nitpick that is not a blocker-- it might be nice since you are specifying "expected.snap", "expected" and "0600" three time each to put each of these in variables: eg, expected_name, expected_contents and expected_perms.

@niemeyer

niemeyer Mar 15, 2016

Contributor

Disagree a bit. Such trivial constants in place make the test more readable compared to a variable we need to look for elsewhere.

An exception is when we have logic that is interdependent but not obviously so. For example, we have two loops that need to have the same number of iterations.

We should fix the inconsistent use of quotes, though ("expected").

osutil/sync_dir_test.go
+
+func (s *EnsureDirStateSuite) TestFixesFilesWithBadPermissions(c *C) {
+ name := filepath.Join(s.dir, "sensitive.snap")
+ // NOTE: the file is wide-open for everyone
@jdstrand

jdstrand Mar 15, 2016

Contributor

Nitpick: "// NOTE: the existing file is currently wide-open for everyone"

osutil/sync_dir_test.go
+ // The content is correct
+ content, err := ioutil.ReadFile(path.Join(s.dir, "expected.snap"))
+ c.Assert(err, IsNil)
+ c.Assert(content, DeepEquals, []byte(`expected`))
@niemeyer

niemeyer Mar 15, 2016

Contributor

For the future: comparing string(content) is better, as []byte will end up not very readable.

+ c.Assert(stat.Mode().Perm(), Equals, os.FileMode(0600))
+}
+
+func (s *EnsureDirStateSuite) TestCorrectsFilesWithSameSize(c *C) {
@niemeyer

niemeyer Mar 15, 2016

Contributor

These tests are all almost exactly the same. For the future, these are much better spelled out as a slice of test cases which define the parameters for a single loop that runs the tests.

var tests = []testCases{{
    existing: ...
    glob:      ...
    content: ...
}

We can even use FileState itself for existing.

But please don't worry about this right now. We've spent way too much time on this functionality already. We'll fix that some day we need to touch this again.

zyga added some commits Mar 16, 2016

osutil: check that EnsureDirState() only has directory-less content
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: do more checks on EnsureDirState() arguments
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: be more strict in writeFile()
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: don't use backtick quotes needlessly
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: tweak comment for readability
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil: describe unsupported security systems
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
osutil/sync_dir.go
@@ -43,6 +43,10 @@ var errSameState = fmt.Errorf("file state has not changed")
// removed. Missing files are created and differing files are corrected. Files
// not matching the pattern are ignored.
//
+// Note, that EnsureDirState() only checks for permissions and content. Other
@niemeyer

niemeyer Mar 17, 2016

Contributor

Note that EnsureDirState only ... (no parenthesis on method or function names, and no comma there)

@niemeyer niemeyer changed the title from osutil: add EnsureDirState() and FileState to osutil: add EnsureDirState and FileState Mar 17, 2016

Contributor

niemeyer commented Mar 17, 2016

One trivial, and LGTM

Contributor

niemeyer commented Mar 17, 2016

@jdstrand Can you please have another look?

+func EnsureDirState(dir, glob string, content map[string]*FileState) (changed, removed []string, err error) {
+ if _, err := filepath.Match(glob, "foo"); err != nil {
+ panic(fmt.Sprintf("EnsureDirState got invalid pattern %q: %s", glob, err))
+ }
@jdstrand

jdstrand Mar 17, 2016

Contributor

Is this debugging code that was left in?

@zyga

zyga Mar 17, 2016

Contributor

No, this is sanity validation.

EDIT: foo is there so that the function does something, it has to be any non-empty value.

@jdstrand

jdstrand Mar 17, 2016

Contributor

This check is non-obvious perhaps since it seems Go isn't providing a way to compile a pattern. It would perhaps be more clear if you checked for ErrBadPattern which according to https://golang.org/pkg/path/filepath/#Match is the only thing it can error on. I would have expected the bad pattern check to happen below with something like:

if ok, err := filepath.Match(glob, baseName); !ok {
    ...
}
if err == ErrBadPattern {
    panic()
}

If you don't like this, can you at least add a comment to make it clear what you are trying to achieve and why you are doing it where you are?

@zyga

zyga Mar 17, 2016

Contributor

I can add a comment. The intent of that check up front, with "foo", was to do it before we actually mess with the files. This would panic mid-way which is not what we want, I think.

@jdstrand

jdstrand Mar 17, 2016

Contributor

But if you do it in the manner I described, the pattern will be wrong on the first check and you panic which shouldn't remove the files?

osutil: tweak comment
Signed-off-by: Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
Contributor

niemeyer commented Mar 17, 2016

Based on the latest comments I assume that the important pieces of this work are sorted out. I'm merging this so we can move forward.

niemeyer added a commit that referenced this pull request Mar 17, 2016

Merge pull request #658 from zyga/fileset-janitor
osutil: add EnsureDirState and FileState

@niemeyer niemeyer merged commit fa7bfe8 into snapcore:master Mar 17, 2016

2 of 5 checks passed

autopkgtest No test results found.
Details
builddeb Build finished. No test results found.
Details
Integration tests Started
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
coverage/coveralls Coverage increased (+0.2%) to 71.573%
Details
+ glob string
+}
+
+var _ = Suite(&EnsureDirStateSuite{glob: "*.snap"})
@jdstrand

jdstrand Mar 17, 2016

Contributor

this should be snap.* now, no?

@niemeyer

niemeyer Mar 17, 2016

Contributor

I'd just replace it by "*.foo" instead, to drop the direct implication that it needs to be inline with what the real call site is using.

+//
+// The content map describes each of the files that are intended to exist in
+// the directory. Map keys must be file names relative to the directory.
+// Sub-directories in the name are not allowed.
@jdstrand

jdstrand Mar 17, 2016

Contributor

I think it would be incredibly useful to give a concrete example of what the arguments are supposed to look like for EnsureDirState here. I'm having to look at the test suite to see how it is intended to be invoked and I think in the future this comment will help people unfamiliar with the code.

@niemeyer

niemeyer Mar 17, 2016

Contributor

@jdstrand Once more, this is a helper. Any part of snappy can call it for its particular needs, with any glob and content. The documentation seems clear about how those call sites can use it. It is the particular call site that is oriented towards writing security profiles which will define how the glob and filenames will look like for that particular purpose.

+ }
+ if ok, _ := filepath.Match(glob, baseName); !ok {
+ panic(fmt.Sprintf("EnsureDirState got filename %q which doesn't match the glob pattern %q", baseName, glob))
+ }
@jdstrand

jdstrand Mar 17, 2016

Contributor

I'm trying to understand the intent of this check. What is filepath.Match(glob, baseName) going to return that doesn't match filepath.Match("snap.*", "snap..")? Shouldn't we instead verify the basename matches a particular snap name.app name regex?

@niemeyer

niemeyer Mar 17, 2016

Contributor

@jdstrand Are you reading my comments? :-)

@zyga zyga deleted the zyga:fileset-janitor branch Mar 17, 2016

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment