From c56b82e529aa587b77f2c059a2c0ae74d399c471 Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Thu, 9 Jul 2020 14:53:49 -0400 Subject: [PATCH] resolvers: add a directory resolver Signed-off-by: Alfredo Deza --- .../scope/{file_resolver.go => resolver.go} | 6 +- imgbom/scope/resolvers/directory_resolver.go | 84 +++++++++ .../resolvers/directory_resolver_test.go | 159 ++++++++++++++++++ 3 files changed, 247 insertions(+), 2 deletions(-) rename imgbom/scope/{file_resolver.go => resolver.go} (81%) create mode 100644 imgbom/scope/resolvers/directory_resolver.go create mode 100644 imgbom/scope/resolvers/directory_resolver_test.go diff --git a/imgbom/scope/file_resolver.go b/imgbom/scope/resolver.go similarity index 81% rename from imgbom/scope/file_resolver.go rename to imgbom/scope/resolver.go index 90c38dd01..913442fd1 100644 --- a/imgbom/scope/file_resolver.go +++ b/imgbom/scope/resolver.go @@ -9,14 +9,16 @@ import ( ) type Resolver interface { - ContentResolver // knows how to get content from file.References - FileResolver // knows how to get file.References from string paths and globs + ContentResolver + FileResolver } +// ContentResolver knows how to get content from file.References type ContentResolver interface { MultipleFileContentsByRef(f ...file.Reference) (map[file.Reference]string, error) } +// FileResolver knows how to get file.References from string paths and globs type FileResolver interface { FilesByPath(paths ...file.Path) ([]file.Reference, error) FilesByGlob(patterns ...string) ([]file.Reference, error) diff --git a/imgbom/scope/resolvers/directory_resolver.go b/imgbom/scope/resolvers/directory_resolver.go new file mode 100644 index 000000000..096be4eb5 --- /dev/null +++ b/imgbom/scope/resolvers/directory_resolver.go @@ -0,0 +1,84 @@ +package resolvers + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "path/filepath" + + "github.com/anchore/imgbom/internal/log" + "github.com/anchore/stereoscope/pkg/file" +) + +type DirectoryResolver struct { + Path string +} + +func (s DirectoryResolver) String() string { + return fmt.Sprintf("dir://%s", s.Path) +} + +func (s DirectoryResolver) FilesByPath(userPaths ...file.Path) ([]file.Reference, error) { + var references = make([]file.Reference, 0) + + for _, userPath := range userPaths { + resolvedPath := path.Join(s.Path, string(userPath)) + _, err := os.Stat(resolvedPath) + if os.IsNotExist(err) { + continue + } else if err != nil { + log.Errorf("path (%s) is not valid: %v", resolvedPath, err) + } + filePath := file.Path(resolvedPath) + references = append(references, file.NewFileReference(filePath)) + } + + return references, nil +} + +func fileContents(path file.Path) ([]byte, error) { + contents, err := ioutil.ReadFile(string(path)) + + if err != nil { + return nil, err + } + return contents, nil +} + +func (s DirectoryResolver) FilesByGlob(patterns ...string) ([]file.Reference, error) { + result := make([]file.Reference, 0) + + for _, pattern := range patterns { + pathPattern := path.Join(s.Path, pattern) + matches, err := filepath.Glob(pathPattern) + if err != nil { + return result, err + } + for _, match := range matches { + fileMeta, err := os.Stat(match) + if err != nil { + continue + } + if fileMeta.IsDir() { + continue + } + matchedPath := file.Path(match) + result = append(result, file.NewFileReference(matchedPath)) + } + } + + return result, nil +} + +func (s DirectoryResolver) MultipleFileContentsByRef(f ...file.Reference) (map[file.Reference]string, error) { + refContents := make(map[file.Reference]string) + for _, fileRef := range f { + contents, err := fileContents(fileRef.Path) + if err != nil { + return refContents, fmt.Errorf("could not read contents of file: %s", fileRef.Path) + } + refContents[fileRef] = string(contents) + } + return refContents, nil +} diff --git a/imgbom/scope/resolvers/directory_resolver_test.go b/imgbom/scope/resolvers/directory_resolver_test.go new file mode 100644 index 000000000..44d67f90f --- /dev/null +++ b/imgbom/scope/resolvers/directory_resolver_test.go @@ -0,0 +1,159 @@ +package resolvers + +import ( + "path" + "testing" + + "github.com/anchore/stereoscope/pkg/file" +) + +func TestDirectoryResolver_FilesByPath(t *testing.T) { + cases := []struct { + name string + input string + refCount int + }{ + { + name: "finds a file", + input: "image-symlinks/file-1.txt", + refCount: 1, + }, + { + name: "managed non-existing files", + input: "image-symlinks/bogus.txt", + refCount: 0, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + resolver := DirectoryResolver{"test-fixtures"} + expected := path.Join("test-fixtures", c.input) + refs, err := resolver.FilesByPath(file.Path(c.input)) + if err != nil { + t.Fatalf("could not use resolver: %+v, %+v", err, refs) + } + + if len(refs) != c.refCount { + t.Errorf("unexpected number of refs: %d != %d", len(refs), c.refCount) + } + + for _, actual := range refs { + if actual.Path != file.Path(expected) { + t.Errorf("bad resolve path: '%s'!='%s'", actual.Path, c.input) + } + } + }) + } +} + +func TestDirectoryResolver_MultipleFilesByPath(t *testing.T) { + cases := []struct { + name string + input []file.Path + refCount int + }{ + { + name: "finds multiple files", + input: []file.Path{file.Path("image-symlinks/file-1.txt"), file.Path("image-symlinks/file-2.txt")}, + refCount: 2, + }, + { + name: "skips non-existing files", + input: []file.Path{file.Path("image-symlinks/bogus.txt"), file.Path("image-symlinks/file-1.txt")}, + refCount: 1, + }, + { + name: "does not return anything for non-existing directories", + input: []file.Path{file.Path("non-existing/bogus.txt"), file.Path("non-existing/file-1.txt")}, + refCount: 0, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + resolver := DirectoryResolver{"test-fixtures"} + + refs, err := resolver.FilesByPath(c.input...) + if err != nil { + t.Fatalf("could not use resolver: %+v, %+v", err, refs) + } + + if len(refs) != c.refCount { + t.Errorf("unexpected number of refs: %d != %d", len(refs), c.refCount) + } + }) + } +} + +func TestDirectoryResolver_MultipleFileContentsByRef(t *testing.T) { + cases := []struct { + name string + input []file.Path + refCount int + contents []string + }{ + { + name: "gets multiple file contents", + input: []file.Path{file.Path("image-symlinks/file-1.txt"), file.Path("image-symlinks/file-2.txt")}, + refCount: 2, + }, + { + name: "skips non-existing files", + input: []file.Path{file.Path("image-symlinks/bogus.txt"), file.Path("image-symlinks/file-1.txt")}, + refCount: 1, + }, + { + name: "does not return anything for non-existing directories", + input: []file.Path{file.Path("non-existing/bogus.txt"), file.Path("non-existing/file-1.txt")}, + refCount: 0, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + refs := make([]file.Reference, 0) + resolver := DirectoryResolver{"test-fixtures"} + + for _, p := range c.input { + refs = append(refs, file.NewFileReference(p)) + } + + contents, err := resolver.MultipleFileContentsByRef(refs...) + if err != nil { + t.Fatalf("unable to generate file contents by ref: %+v", err) + } + if len(contents) != c.refCount { + t.Errorf("unexpected number of refs produced: %d != %d", len(contents), c.refCount) + } + + }) + } +} + +func TestDirectoryResolver_FilesByGlobMultiple(t *testing.T) { + t.Run("finds multiple matching files", func(t *testing.T) { + resolver := DirectoryResolver{"test-fixtures"} + refs, err := resolver.FilesByGlob("image-symlinks/file*") + if err != nil { + t.Fatalf("could not use resolver: %+v, %+v", err, refs) + } + + if len(refs) != 2 { + t.Errorf("unexpected number of refs: %d != 2", len(refs)) + } + + }) +} + +func TestDirectoryResolver_FilesByGlobSingle(t *testing.T) { + t.Run("finds multiple matching files", func(t *testing.T) { + resolver := DirectoryResolver{"test-fixtures"} + refs, err := resolver.FilesByGlob("image-symlinks/*1.txt") + if err != nil { + t.Fatalf("could not use resolver: %+v, %+v", err, refs) + } + + if len(refs) != 1 { + t.Errorf("unexpected number of refs: %d != 1", len(refs)) + } + + }) +}