Merge pull request #305 from anchore/add-has-path-to-resolver

Add HasPath() to Resolver interface for existence check
This commit is contained in:
Alex Goodman 2021-01-04 19:45:03 -05:00 committed by GitHub
commit bb70b0b43e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 146 additions and 64 deletions

2
go.mod
View File

@ -9,7 +9,7 @@ require (
github.com/anchore/go-rpmdb v0.0.0-20201106153645-0043963c2e12 github.com/anchore/go-rpmdb v0.0.0-20201106153645-0043963c2e12
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/stereoscope v0.0.0-20210104203718-4c1d1bd9a255 github.com/anchore/stereoscope v0.0.0-20210105001222-7beea73cb7e5
github.com/antihax/optional v1.0.0 github.com/antihax/optional v1.0.0
github.com/bmatcuk/doublestar/v2 v2.0.4 github.com/bmatcuk/doublestar/v2 v2.0.4
github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible

4
go.sum
View File

@ -136,6 +136,10 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
github.com/anchore/stereoscope v0.0.0-20210104203718-4c1d1bd9a255 h1:Ng7BDr9PQTCztANogjfEdEjjWUylhlPyZPhtarIGo00= github.com/anchore/stereoscope v0.0.0-20210104203718-4c1d1bd9a255 h1:Ng7BDr9PQTCztANogjfEdEjjWUylhlPyZPhtarIGo00=
github.com/anchore/stereoscope v0.0.0-20210104203718-4c1d1bd9a255/go.mod h1:BMdPL0QEIYfpjQ3M7sHYZvuh6+vcomqF3TMHL8gr6Vw= github.com/anchore/stereoscope v0.0.0-20210104203718-4c1d1bd9a255/go.mod h1:BMdPL0QEIYfpjQ3M7sHYZvuh6+vcomqF3TMHL8gr6Vw=
github.com/anchore/stereoscope v0.0.0-20210105000809-428eda0b2ec6 h1:JWpsV/8x1fuCYjJmNjT43cVFblLTpO/ISDnePukiTNw=
github.com/anchore/stereoscope v0.0.0-20210105000809-428eda0b2ec6/go.mod h1:BMdPL0QEIYfpjQ3M7sHYZvuh6+vcomqF3TMHL8gr6Vw=
github.com/anchore/stereoscope v0.0.0-20210105001222-7beea73cb7e5 h1:NGRfS6BZKElgiMbqdoH9iQn+6oxT7CJdZYrqgwvGkWY=
github.com/anchore/stereoscope v0.0.0-20210105001222-7beea73cb7e5/go.mod h1:BMdPL0QEIYfpjQ3M7sHYZvuh6+vcomqF3TMHL8gr6Vw=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=

View File

@ -21,6 +21,10 @@ func newTestResolver() *testResolverMock {
} }
} }
func (r testResolverMock) HasPath(path string) bool {
panic("not implemented")
}
func (r *testResolverMock) FileContentsByLocation(_ source.Location) (io.ReadCloser, error) { func (r *testResolverMock) FileContentsByLocation(_ source.Location) (io.ReadCloser, error) {
return nil, fmt.Errorf("not implemented") return nil, fmt.Errorf("not implemented")
} }

View File

@ -45,11 +45,6 @@ func parseRpmDB(resolver source.FileResolver, dbLocation source.Location, reader
allPkgs := make([]pkg.Package, 0) allPkgs := make([]pkg.Package, 0)
for _, entry := range pkgList { for _, entry := range pkgList {
records, err := extractRpmdbFileRecords(resolver, entry)
if err != nil {
return nil, err
}
p := pkg.Package{ p := pkg.Package{
Name: entry.Name, Name: entry.Name,
Version: fmt.Sprintf("%s-%s", entry.Version, entry.Release), // this is what engine does, instead of fmt.Sprintf("%d:%s-%s.%s", entry.Epoch, entry.Version, entry.Release, entry.Arch) Version: fmt.Sprintf("%s-%s", entry.Version, entry.Release), // this is what engine does, instead of fmt.Sprintf("%d:%s-%s.%s", entry.Epoch, entry.Version, entry.Release, entry.Arch)
@ -67,7 +62,7 @@ func parseRpmDB(resolver source.FileResolver, dbLocation source.Location, reader
Vendor: entry.Vendor, Vendor: entry.Vendor,
License: entry.License, License: entry.License,
Size: entry.Size, Size: entry.Size,
Files: records, Files: extractRpmdbFileRecords(resolver, entry),
}, },
} }
@ -77,19 +72,12 @@ func parseRpmDB(resolver source.FileResolver, dbLocation source.Location, reader
return allPkgs, nil return allPkgs, nil
} }
func extractRpmdbFileRecords(resolver source.FileResolver, entry *rpmdb.PackageInfo) ([]pkg.RpmdbFileRecord, error) { func extractRpmdbFileRecords(resolver source.FileResolver, entry *rpmdb.PackageInfo) []pkg.RpmdbFileRecord {
var records = make([]pkg.RpmdbFileRecord, 0) var records = make([]pkg.RpmdbFileRecord, 0)
for _, record := range entry.Files { for _, record := range entry.Files {
refs, err := resolver.FilesByPath(record.Path)
if err != nil {
return nil, fmt.Errorf("failed to resolve path=%+v: %w", record.Path, err)
}
//only persist RPMDB file records which exist in the image/directory, otherwise ignore them //only persist RPMDB file records which exist in the image/directory, otherwise ignore them
if len(refs) == 0 { if resolver.HasPath(record.Path) {
continue
}
records = append(records, pkg.RpmdbFileRecord{ records = append(records, pkg.RpmdbFileRecord{
Path: record.Path, Path: record.Path,
Mode: pkg.RpmdbFileMode(record.Mode), Mode: pkg.RpmdbFileMode(record.Mode),
@ -97,5 +85,6 @@ func extractRpmdbFileRecords(resolver source.FileResolver, entry *rpmdb.PackageI
SHA256: record.SHA256, SHA256: record.SHA256,
}) })
} }
return records, nil }
return records
} }

View File

@ -21,6 +21,10 @@ func newTestFileResolver(ignorePaths bool) *rpmdbTestFileResolverMock {
} }
} }
func (r rpmdbTestFileResolverMock) HasPath(path string) bool {
return !r.ignorePaths
}
func (r *rpmdbTestFileResolverMock) FilesByPath(paths ...string) ([]source.Location, error) { func (r *rpmdbTestFileResolverMock) FilesByPath(paths ...string) ([]source.Location, error) {
if r.ignorePaths { if r.ignorePaths {
// act as if no paths exist // act as if no paths exist

View File

@ -5,12 +5,10 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/anchore/stereoscope/pkg/filetree"
"github.com/anchore/syft/internal/log"
"github.com/anchore/stereoscope/pkg/file" "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/filetree"
"github.com/anchore/stereoscope/pkg/image" "github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/internal/log"
) )
var _ Resolver = (*AllLayersResolver)(nil) var _ Resolver = (*AllLayersResolver)(nil)
@ -37,6 +35,18 @@ func NewAllLayersResolver(img *image.Image) (*AllLayersResolver, error) {
}, nil }, nil
} }
// HasPath indicates if the given path exists in the underlying source.
func (r *AllLayersResolver) HasPath(path string) bool {
p := file.Path(path)
for _, layerIdx := range r.layers {
tree := r.img.Layers[layerIdx].Tree
if tree.HasPath(p) {
return true
}
}
return false
}
func (r *AllLayersResolver) fileByRef(ref file.Reference, uniqueFileIDs file.ReferenceSet, layerIdx int) ([]file.Reference, error) { func (r *AllLayersResolver) fileByRef(ref file.Reference, uniqueFileIDs file.ReferenceSet, layerIdx int) ([]file.Reference, error) {
uniqueFiles := make([]file.Reference, 0) uniqueFiles := make([]file.Reference, 0)

View File

@ -16,6 +16,7 @@ func TestAllLayersResolver_FilesByPath(t *testing.T) {
name string name string
linkPath string linkPath string
resolutions []resolution resolutions []resolution
forcePositiveHasPath bool
}{ }{
{ {
name: "link with previous data", name: "link with previous data",
@ -69,11 +70,14 @@ func TestAllLayersResolver_FilesByPath(t *testing.T) {
name: "dead link", name: "dead link",
linkPath: "/link-dead", linkPath: "/link-dead",
resolutions: []resolution{}, resolutions: []resolution{},
forcePositiveHasPath: true,
}, },
{ {
name: "ignore directories", name: "ignore directories",
linkPath: "/bin", linkPath: "/bin",
resolutions: []resolution{}, resolutions: []resolution{},
// directories don't resolve BUT do exist
forcePositiveHasPath: true,
}, },
} }
for _, c := range cases { for _, c := range cases {
@ -86,6 +90,17 @@ func TestAllLayersResolver_FilesByPath(t *testing.T) {
t.Fatalf("could not create resolver: %+v", err) t.Fatalf("could not create resolver: %+v", err)
} }
hasPath := resolver.HasPath(c.linkPath)
if !c.forcePositiveHasPath {
if len(c.resolutions) > 0 && !hasPath {
t.Errorf("expected HasPath() to indicate existance, but did not")
} else if len(c.resolutions) == 0 && hasPath {
t.Errorf("expeced HasPath() to NOT indicate existance, but does")
}
} else if !hasPath {
t.Errorf("expected HasPath() to indicate existance, but did not (force path)")
}
refs, err := resolver.FilesByPath(c.linkPath) refs, err := resolver.FilesByPath(c.linkPath)
if err != nil { if err != nil {
t.Fatalf("could not use resolver: %+v", err) t.Fatalf("could not use resolver: %+v", err)

View File

@ -19,27 +19,37 @@ type DirectoryResolver struct {
Path string Path string
} }
func (r DirectoryResolver) requestPath(userPath string) string {
fullPath := userPath
if filepath.IsAbs(fullPath) {
// a path relative to root should be prefixed with the resolvers directory path, otherwise it should be left as is
fullPath = path.Join(r.Path, fullPath)
}
return fullPath
}
// HasPath indicates if the given path exists in the underlying source.
func (r *DirectoryResolver) HasPath(userPath string) bool {
_, err := os.Stat(r.requestPath(userPath))
return !os.IsNotExist(err)
}
// Stringer to represent a directory path data source // Stringer to represent a directory path data source
func (s DirectoryResolver) String() string { func (r DirectoryResolver) String() string {
return fmt.Sprintf("dir:%s", s.Path) return fmt.Sprintf("dir:%s", r.Path)
} }
// FilesByPath returns all file.References that match the given paths from the directory. // FilesByPath returns all file.References that match the given paths from the directory.
func (s DirectoryResolver) FilesByPath(userPaths ...string) ([]Location, error) { func (r DirectoryResolver) FilesByPath(userPaths ...string) ([]Location, error) {
var references = make([]Location, 0) var references = make([]Location, 0)
for _, userPath := range userPaths { for _, userPath := range userPaths {
userStrPath := userPath userStrPath := r.requestPath(userPath)
if filepath.IsAbs(userStrPath) {
// a path relative to root should be prefixed with the resolvers directory path, otherwise it should be left as is
userStrPath = path.Join(s.Path, userStrPath)
}
fileMeta, err := os.Stat(userStrPath) fileMeta, err := os.Stat(userStrPath)
if os.IsNotExist(err) { if os.IsNotExist(err) {
continue continue
} else if err != nil { } else if err != nil {
log.Errorf("path (%s) is not valid: %v", userStrPath, err) log.Errorf("path (%r) is not valid: %v", userStrPath, err)
} }
// don't consider directories // don't consider directories
@ -54,11 +64,11 @@ func (s DirectoryResolver) FilesByPath(userPaths ...string) ([]Location, error)
} }
// FilesByGlob returns all file.References that match the given path glob pattern from any layer in the image. // FilesByGlob returns all file.References that match the given path glob pattern from any layer in the image.
func (s DirectoryResolver) FilesByGlob(patterns ...string) ([]Location, error) { func (r DirectoryResolver) FilesByGlob(patterns ...string) ([]Location, error) {
result := make([]Location, 0) result := make([]Location, 0)
for _, pattern := range patterns { for _, pattern := range patterns {
pathPattern := path.Join(s.Path, pattern) pathPattern := path.Join(r.Path, pattern)
pathMatches, err := doublestar.Glob(pathPattern) pathMatches, err := doublestar.Glob(pathPattern)
if err != nil { if err != nil {
return nil, err return nil, err
@ -84,8 +94,8 @@ func (s DirectoryResolver) FilesByGlob(patterns ...string) ([]Location, error) {
// RelativeFileByPath fetches a single file at the given path relative to the layer squash of the given reference. // RelativeFileByPath fetches a single file at the given path relative to the layer squash of the given reference.
// This is helpful when attempting to find a file that is in the same layer or lower as another file. For the // This is helpful when attempting to find a file that is in the same layer or lower as another file. For the
// DirectoryResolver, this is a simple path lookup. // DirectoryResolver, this is a simple path lookup.
func (s *DirectoryResolver) RelativeFileByPath(_ Location, path string) *Location { func (r *DirectoryResolver) RelativeFileByPath(_ Location, path string) *Location {
paths, err := s.FilesByPath(path) paths, err := r.FilesByPath(path)
if err != nil { if err != nil {
return nil return nil
} }
@ -97,7 +107,7 @@ func (s *DirectoryResolver) RelativeFileByPath(_ Location, path string) *Locatio
} }
// MultipleFileContentsByLocation returns the file contents for all file.References relative a directory. // MultipleFileContentsByLocation returns the file contents for all file.References relative a directory.
func (s DirectoryResolver) MultipleFileContentsByLocation(locations []Location) (map[Location]io.ReadCloser, error) { func (r DirectoryResolver) MultipleFileContentsByLocation(locations []Location) (map[Location]io.ReadCloser, error) {
refContents := make(map[Location]io.ReadCloser) refContents := make(map[Location]io.ReadCloser)
for _, location := range locations { for _, location := range locations {
refContents[location] = file.NewDeferredReadCloser(location.Path) refContents[location] = file.NewDeferredReadCloser(location.Path)
@ -107,6 +117,6 @@ func (s DirectoryResolver) MultipleFileContentsByLocation(locations []Location)
// FileContentsByLocation fetches file contents for a single file reference relative to a directory. // FileContentsByLocation fetches file contents for a single file reference relative to a directory.
// If the path does not exist an error is returned. // If the path does not exist an error is returned.
func (s DirectoryResolver) FileContentsByLocation(location Location) (io.ReadCloser, error) { func (r DirectoryResolver) FileContentsByLocation(location Location) (io.ReadCloser, error) {
return file.NewDeferredReadCloser(location.Path), nil return file.NewDeferredReadCloser(location.Path), nil
} }

View File

@ -11,6 +11,7 @@ func TestDirectoryResolver_FilesByPath(t *testing.T) {
input string input string
expected string expected string
refCount int refCount int
forcePositiveHasPath bool
}{ }{
{ {
name: "finds a file (relative)", name: "finds a file (relative)",
@ -51,11 +52,24 @@ func TestDirectoryResolver_FilesByPath(t *testing.T) {
root: "./test-fixtures/", root: "./test-fixtures/",
input: "/image-symlinks", input: "/image-symlinks",
refCount: 0, refCount: 0,
forcePositiveHasPath: true,
}, },
} }
for _, c := range cases { for _, c := range cases {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
resolver := DirectoryResolver{c.root} resolver := DirectoryResolver{c.root}
hasPath := resolver.HasPath(c.input)
if !c.forcePositiveHasPath {
if c.refCount != 0 && !hasPath {
t.Errorf("expected HasPath() to indicate existance, but did not")
} else if c.refCount == 0 && hasPath {
t.Errorf("expeced HasPath() to NOT indicate existance, but does")
}
} else if !hasPath {
t.Errorf("expected HasPath() to indicate existance, but did not (force path)")
}
refs, err := resolver.FilesByPath(c.input) refs, err := resolver.FilesByPath(c.input)
if err != nil { if err != nil {
t.Fatalf("could not use resolver: %+v, %+v", err, refs) t.Fatalf("could not use resolver: %+v, %+v", err, refs)

View File

@ -4,9 +4,8 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/anchore/stereoscope/pkg/filetree"
"github.com/anchore/stereoscope/pkg/file" "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/filetree"
"github.com/anchore/stereoscope/pkg/image" "github.com/anchore/stereoscope/pkg/image"
) )
@ -25,6 +24,11 @@ func NewImageSquashResolver(img *image.Image) (*ImageSquashResolver, error) {
return &ImageSquashResolver{img: img}, nil return &ImageSquashResolver{img: img}, nil
} }
// HasPath indicates if the given path exists in the underlying source.
func (r *ImageSquashResolver) HasPath(path string) bool {
return r.img.SquashedTree().HasPath(file.Path(path))
}
// FilesByPath returns all file.References that match the given paths within the squashed representation of the image. // FilesByPath returns all file.References that match the given paths within the squashed representation of the image.
func (r *ImageSquashResolver) FilesByPath(paths ...string) ([]Location, error) { func (r *ImageSquashResolver) FilesByPath(paths ...string) ([]Location, error) {
uniqueFileIDs := file.NewFileReferenceSet() uniqueFileIDs := file.NewFileReferenceSet()

View File

@ -12,6 +12,7 @@ func TestImageSquashResolver_FilesByPath(t *testing.T) {
linkPath string linkPath string
resolveLayer uint resolveLayer uint
resolvePath string resolvePath string
forcePositiveHasPath bool
}{ }{
{ {
name: "link with previous data", name: "link with previous data",
@ -42,11 +43,15 @@ func TestImageSquashResolver_FilesByPath(t *testing.T) {
linkPath: "/link-dead", linkPath: "/link-dead",
resolveLayer: 8, resolveLayer: 8,
resolvePath: "", resolvePath: "",
// the path should exist, even if the link is dead
forcePositiveHasPath: true,
}, },
{ {
name: "ignore directories", name: "ignore directories",
linkPath: "/bin", linkPath: "/bin",
resolvePath: "", resolvePath: "",
// the path should exist, even if we ignore it
forcePositiveHasPath: true,
}, },
{ {
name: "parent is a link (with overridden data)", name: "parent is a link (with overridden data)",
@ -65,6 +70,17 @@ func TestImageSquashResolver_FilesByPath(t *testing.T) {
t.Fatalf("could not create resolver: %+v", err) t.Fatalf("could not create resolver: %+v", err)
} }
hasPath := resolver.HasPath(c.linkPath)
if !c.forcePositiveHasPath {
if c.resolvePath != "" && !hasPath {
t.Errorf("expected HasPath() to indicate existance, but did not")
} else if c.resolvePath == "" && hasPath {
t.Errorf("expeced HasPath() to NOT indicate existance, but does")
}
} else if !hasPath {
t.Errorf("expected HasPath() to indicate existance, but did not (force path)")
}
refs, err := resolver.FilesByPath(c.linkPath) refs, err := resolver.FilesByPath(c.linkPath)
if err != nil { if err != nil {
t.Fatalf("could not use resolver: %+v", err) t.Fatalf("could not use resolver: %+v", err)

View File

@ -28,6 +28,16 @@ func NewMockResolverForPaths(paths ...string) *MockResolver {
return &MockResolver{Locations: locations} return &MockResolver{Locations: locations}
} }
// HasPath indicates if the given path exists in the underlying source.
func (r MockResolver) HasPath(path string) bool {
for _, l := range r.Locations {
if l.Path == path {
return true
}
}
return false
}
// String returns the string representation of the MockResolver. // String returns the string representation of the MockResolver.
func (r MockResolver) String() string { func (r MockResolver) String() string {
return fmt.Sprintf("mock:(%s,...)", r.Locations[0].Path) return fmt.Sprintf("mock:(%s,...)", r.Locations[0].Path)

View File

@ -20,8 +20,10 @@ type ContentResolver interface {
// TODO: we should consider refactoring to return a set of io.Readers or file.Openers instead of the full contents themselves (allow for optional buffering). // TODO: we should consider refactoring to return a set of io.Readers or file.Openers instead of the full contents themselves (allow for optional buffering).
} }
// FileResolver knows how to get file.References for given string paths and globs // FileResolver knows how to get a Location for given string paths and globs
type FileResolver interface { type FileResolver interface {
// HasPath indicates if the given path exists in the underlying source.
HasPath(path string) bool
// FilesByPath fetches a set of file references which have the given path (for an image, there may be multiple matches) // FilesByPath fetches a set of file references which have the given path (for an image, there may be multiple matches)
FilesByPath(paths ...string) ([]Location, error) FilesByPath(paths ...string) ([]Location, error)
// FilesByGlob fetches a set of file references which the given glob matches // FilesByGlob fetches a set of file references which the given glob matches