Merge pull request #68 from anchore/issue-31-1

Add support for file-based contents
This commit is contained in:
Alfredo Deza 2020-07-02 17:29:45 -04:00 committed by GitHub
commit fe338760b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1028 additions and 59 deletions

View File

@ -17,13 +17,14 @@ import (
) )
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: fmt.Sprintf("%s [IMAGE]", internal.ApplicationName), Use: fmt.Sprintf("%s [SOURCE]", internal.ApplicationName),
Short: "A container image BOM tool", // TODO: add copy Short: "A tool that generates a Software Build Of Materials (SBOM)",
Long: internal.Tprintf(`\ Long: internal.Tprintf(`\
Supports the following image sources: Supports the following image sources:
{{.appName}} yourrepo/yourimage:tag defaults to using images from a docker daemon {{.appName}} yourrepo/yourimage:tag defaults to using images from a docker daemon
{{.appName}} docker://yourrepo/yourimage:tag explicitly use the docker daemon {{.appName}} docker://yourrepo/yourimage:tag explicitly use the docker daemon
{{.appName}} tar://path/to/yourimage.tar use a tarball from disk {{.appName}} tar://path/to/yourimage.tar use a tarball from disk
{{.appName}} dir://path/to/yourproject read directly from a path in disk
`, map[string]interface{}{ `, map[string]interface{}{
"appName": internal.ApplicationName, "appName": internal.ApplicationName,
}), }),
@ -44,38 +45,61 @@ func init() {
) )
} }
func startWorker(userImage string) <-chan error { func startWorker(userInput string) <-chan error {
errs := make(chan error) errs := make(chan error)
go func() { go func() {
defer close(errs) defer close(errs)
protocol := imgbom.NewProtocol(userInput)
fmt.Printf("protocol: %+v", protocol)
log.Infof("Fetching image '%s'", userImage) switch protocol.Type {
img, err := stereoscope.GetImage(userImage) case imgbom.DirProtocol:
if err != nil {
errs <- fmt.Errorf("could not fetch image '%s': %w", userImage, err) log.Info("Cataloging directory")
return catalog, err := imgbom.CatalogDir(protocol.Value, appConfig.ScopeOpt)
if err != nil {
errs <- fmt.Errorf("could not produce catalog: %w", err)
}
bus.Publish(partybus.Event{
Type: event.CatalogerFinished,
Value: presenter.GetDirPresenter(appConfig.PresenterOpt, protocol.Value, catalog),
})
default:
log.Infof("Fetching image '%s'", userInput)
img, err := stereoscope.GetImage(userInput)
if err != nil || img == nil {
errs <- fmt.Errorf("could not fetch image '%s': %w", userInput, err)
// TODO: this needs to be handled better
bus.Publish(partybus.Event{
Type: event.CatalogerFinished,
Value: nil,
})
return
}
defer stereoscope.Cleanup()
log.Info("Identifying Distro")
distro := imgbom.IdentifyDistro(img)
if distro == nil {
log.Errorf("error identifying distro")
} else {
log.Infof(" Distro: %s", distro)
}
log.Info("Cataloging Image")
catalog, err := imgbom.CatalogImg(img, appConfig.ScopeOpt)
if err != nil {
errs <- fmt.Errorf("could not produce catalog: %w", err)
}
bus.Publish(partybus.Event{
Type: event.CatalogerFinished,
Value: presenter.GetImgPresenter(appConfig.PresenterOpt, img, catalog),
})
} }
defer stereoscope.Cleanup()
log.Info("Identifying Distro")
distro := imgbom.IdentifyDistro(img)
if distro == nil {
log.Errorf("error identifying distro")
} else {
log.Infof(" Distro: %s", distro)
}
log.Info("Cataloging image")
catalog, err := imgbom.CatalogImage(img, appConfig.ScopeOpt)
if err != nil {
errs <- fmt.Errorf("could not catalog image: %w", err)
}
log.Info("Complete!")
bus.Publish(partybus.Event{
Type: event.CatalogerFinished,
Value: presenter.GetPresenter(appConfig.PresenterOpt, img, catalog),
})
}() }()
return errs return errs
} }

View File

@ -29,7 +29,7 @@ func Catalogers() []string {
return c return c
} }
func Catalog(s scope.Scope) (*pkg.Catalog, error) { func Catalog(s scope.FileContentResolver) (*pkg.Catalog, error) {
return controllerInstance.catalog(s) return controllerInstance.catalog(s)
} }
@ -71,7 +71,7 @@ func (c *controller) trackCataloger() (*progress.Manual, *progress.Manual) {
return &filesProcessed, &packagesDiscovered return &filesProcessed, &packagesDiscovered
} }
func (c *controller) catalog(s scope.Scope) (*pkg.Catalog, error) { func (c *controller) catalog(s scope.FileContentResolver) (*pkg.Catalog, error) {
catalog := pkg.NewCatalog() catalog := pkg.NewCatalog()
fileSelection := make([]file.Reference, 0) fileSelection := make([]file.Reference, 0)
@ -79,13 +79,13 @@ func (c *controller) catalog(s scope.Scope) (*pkg.Catalog, error) {
// ask catalogers for files to extract from the image tar // ask catalogers for files to extract from the image tar
for _, a := range c.catalogers { for _, a := range c.catalogers {
fileSelection = append(fileSelection, a.SelectFiles(&s)...) fileSelection = append(fileSelection, a.SelectFiles(s)...)
log.Debugf("cataloger '%s' selected '%d' files", a.Name(), len(fileSelection)) log.Debugf("cataloger '%s' selected '%d' files", a.Name(), len(fileSelection))
filesProcessed.N += int64(len(fileSelection)) filesProcessed.N += int64(len(fileSelection))
} }
// fetch contents for requested selection by catalogers // fetch contents for requested selection by catalogers
contents, err := s.Image.MultipleFileContentsByRef(fileSelection...) contents, err := s.MultipleFileContentsByRef(fileSelection...)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -24,7 +24,7 @@ func Identify(img *image.Image) *Distro {
} }
for path, fn := range identityFiles { for path, fn := range identityFiles {
contents, err := img.FileContentsFromSquash(path) contents, err := img.FileContentsFromSquash(path) // TODO: this call replaced with "MultipleFileContents"
if err != nil { if err != nil {
log.Debugf("unable to get contents from %s: %s", path, err) log.Debugf("unable to get contents from %s: %s", path, err)

View File

@ -16,12 +16,19 @@ func IdentifyDistro(img *image.Image) *distro.Distro {
return distro.Identify(img) return distro.Identify(img)
} }
func CatalogImage(img *image.Image, o scope.Option) (*pkg.Catalog, error) { func CatalogDir(d string, o scope.Option) (*pkg.Catalog, error) {
s, err := scope.NewScope(img, o) s, err := scope.NewDirScope(d, o)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return cataloger.Catalog(s)
}
func CatalogImg(img *image.Image, o scope.Option) (*pkg.Catalog, error) {
s, err := scope.NewImageScope(img, o)
if err != nil {
return nil, err
}
return cataloger.Catalog(s) return cataloger.Catalog(s)
} }

View File

@ -0,0 +1,77 @@
package dirs
import (
"encoding/json"
"io"
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/anchore/imgbom/internal/log"
)
type Presenter struct {
catalog *pkg.Catalog
path string
}
func NewPresenter(catalog *pkg.Catalog, path string) *Presenter {
return &Presenter{
catalog: catalog,
path: path,
}
}
type document struct {
Artifacts []artifact `json:"artifacts"`
Source string
}
type source struct {
FoundBy string `json:"foundBy"`
Effects []string `json:"effects"`
}
type artifact struct {
Name string `json:"name"`
Version string `json:"version"`
Type string `json:"type"`
Cataloger string `json:"cataloger"`
Sources []source `json:"sources"`
Metadata interface{} `json:"metadata"`
}
func (pres *Presenter) Present(output io.Writer) error {
doc := document{
Artifacts: make([]artifact, 0),
Source: pres.path,
}
// populate artifacts...
// TODO: move this into a common package so that other text presenters can reuse
for p := range pres.catalog.Enumerate() {
art := artifact{
Name: p.Name,
Version: p.Version,
Type: p.Type.String(),
Sources: make([]source, len(p.Source)),
Metadata: p.Metadata,
}
for idx := range p.Source {
srcObj := source{
FoundBy: p.FoundBy,
Effects: []string{}, // TODO
}
art.Sources[idx] = srcObj
}
doc.Artifacts = append(doc.Artifacts, art)
}
bytes, err := json.Marshal(&doc)
if err != nil {
log.Errorf("failed to marshal json (presenter=json): %w", err)
}
_, err = output.Write(bytes)
return err
}

View File

@ -0,0 +1,53 @@
package dirs
import (
"bytes"
"flag"
"testing"
"github.com/anchore/go-testutils"
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/sergi/go-diff/diffmatchpatch"
)
var update = flag.Bool("update", false, "update the *.golden files for json presenters")
func TestJsonPresenter(t *testing.T) {
var buffer bytes.Buffer
catalog := pkg.NewCatalog()
// populate catalog with test data
catalog.Add(pkg.Package{
Name: "package-1",
Version: "1.0.1",
Type: pkg.DebPkg,
})
catalog.Add(pkg.Package{
Name: "package-2",
Version: "2.0.1",
Type: pkg.DebPkg,
})
pres := NewPresenter(catalog, "/some/path")
// run presenter
err := pres.Present(&buffer)
if err != nil {
t.Fatal(err)
}
actual := buffer.Bytes()
if *update {
testutils.UpdateGoldenFileContents(t, actual)
}
var expected = testutils.GetGoldenFileContents(t)
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(actual), string(expected), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
}

View File

@ -0,0 +1 @@
{"artifacts":[{"name":"package-1","version":"1.0.1","type":"deb","cataloger":"","sources":[],"metadata":null},{"name":"package-2","version":"2.0.1","type":"deb","cataloger":"","sources":[],"metadata":null}],"Source":"/some/path"}

View File

@ -1,7 +1,8 @@
package json package imgs
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"github.com/anchore/imgbom/imgbom/pkg" "github.com/anchore/imgbom/imgbom/pkg"
@ -82,6 +83,7 @@ func (pres *Presenter) Present(output io.Writer) error {
} }
// populate artifacts... // populate artifacts...
// TODO: move this into a common package so that other text presenters can reuse
for p := range pres.catalog.Enumerate() { for p := range pres.catalog.Enumerate() {
art := artifact{ art := artifact{
Name: p.Name, Name: p.Name,
@ -93,14 +95,17 @@ func (pres *Presenter) Present(output io.Writer) error {
for idx, src := range p.Source { for idx, src := range p.Source {
fileMetadata, err := pres.img.FileCatalog.Get(src) fileMetadata, err := pres.img.FileCatalog.Get(src)
var layer int
if err != nil { if err != nil {
// TODO: test case // TODO: test case
log.Errorf("could not get metadata from catalog (presenter=json): %+v", src) return fmt.Errorf("could not get metadata from catalog (presenter=json src=%v): %w", src, err)
} }
layer = int(fileMetadata.Source.Metadata.Index)
srcObj := source{ srcObj := source{
FoundBy: p.FoundBy, FoundBy: p.FoundBy,
Layer: int(fileMetadata.Source.Metadata.Index), Layer: layer,
Effects: []string{}, // TODO Effects: []string{}, // TODO
} }
art.Sources[idx] = srcObj art.Sources[idx] = srcObj

View File

@ -1,4 +1,4 @@
package json package imgs
import ( import (
"bytes" "bytes"

View File

@ -4,8 +4,10 @@ import (
"io" "io"
"github.com/anchore/imgbom/imgbom/pkg" "github.com/anchore/imgbom/imgbom/pkg"
"github.com/anchore/imgbom/imgbom/presenter/json" json_dirs "github.com/anchore/imgbom/imgbom/presenter/json/dirs"
"github.com/anchore/imgbom/imgbom/presenter/text" json_imgs "github.com/anchore/imgbom/imgbom/presenter/json/imgs"
text_dirs "github.com/anchore/imgbom/imgbom/presenter/text/dirs"
text_imgs "github.com/anchore/imgbom/imgbom/presenter/text/imgs"
"github.com/anchore/stereoscope/pkg/image" "github.com/anchore/stereoscope/pkg/image"
) )
@ -13,13 +15,23 @@ type Presenter interface {
Present(io.Writer) error Present(io.Writer) error
} }
func GetPresenter(option Option, img *image.Image, catalog *pkg.Catalog) Presenter { func GetImgPresenter(option Option, img *image.Image, catalog *pkg.Catalog) Presenter {
switch option { switch option {
case JSONPresenter: case JSONPresenter:
return json.NewPresenter(img, catalog) return json_imgs.NewPresenter(img, catalog)
case TextPresenter: case TextPresenter:
return text.NewPresenter(img, catalog) return text_imgs.NewPresenter(img, catalog)
default:
return nil
}
}
func GetDirPresenter(option Option, path string, catalog *pkg.Catalog) Presenter {
switch option {
case JSONPresenter:
return json_dirs.NewPresenter(catalog, path)
case TextPresenter:
return text_dirs.NewPresenter(catalog, path)
default: default:
return nil return nil
} }

View File

@ -0,0 +1,45 @@
package text
import (
"fmt"
"io"
"text/tabwriter"
"github.com/anchore/imgbom/imgbom/pkg"
)
type Presenter struct {
catalog *pkg.Catalog
path string
}
func NewPresenter(catalog *pkg.Catalog, path string) *Presenter {
return &Presenter{
catalog: catalog,
path: path,
}
}
// Present is a method that is in charge of writing to an output buffer
func (pres *Presenter) Present(output io.Writer) error {
// init the tabular writer
w := new(tabwriter.Writer)
w.Init(output, 0, 8, 0, '\t', tabwriter.AlignRight)
fmt.Fprintln(w, fmt.Sprintf("[Path: %s]", pres.path))
// populate artifacts...
// TODO: move this into a common package so that other text presenters can reuse
for p := range pres.catalog.Enumerate() {
fmt.Fprintln(w, fmt.Sprintf("[%s]", p.Name))
fmt.Fprintln(w, " Version:\t", p.Version)
fmt.Fprintln(w, " Type:\t", p.Type.String())
if p.Metadata != nil {
fmt.Fprintf(w, " Metadata:\t%+v\n", p.Metadata)
}
fmt.Fprintln(w, " Found by:\t", p.FoundBy)
fmt.Fprintln(w)
w.Flush()
}
return nil
}

View File

@ -0,0 +1,53 @@
package text
import (
"bytes"
"flag"
"testing"
"github.com/anchore/go-testutils"
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/sergi/go-diff/diffmatchpatch"
)
var update = flag.Bool("update", false, "update the *.golden files for json presenters")
func TestTextPresenter(t *testing.T) {
var buffer bytes.Buffer
catalog := pkg.NewCatalog()
// populate catalog with test data
catalog.Add(pkg.Package{
Name: "package-1",
Version: "1.0.1",
Type: pkg.DebPkg,
})
catalog.Add(pkg.Package{
Name: "package-2",
Version: "2.0.1",
Type: pkg.DebPkg,
})
pres := NewPresenter(catalog, "/some/path")
// run presenter
err := pres.Present(&buffer)
if err != nil {
t.Fatal(err)
}
actual := buffer.Bytes()
if *update {
testutils.UpdateGoldenFileContents(t, actual)
}
var expected = testutils.GetGoldenFileContents(t)
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(actual), string(expected), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
}

View File

@ -0,0 +1 @@
{"artifacts":[{"name":"package-1","version":"1.0.1","type":"deb","cataloger":"","sources":[],"metadata":null},{"name":"package-2","version":"2.0.1","type":"deb","cataloger":"","sources":[],"metadata":null}],"Source":"/some/path"}

View File

@ -0,0 +1,11 @@
[Path: /some/path]
[package-1]
Version: 1.0.1
Type: deb
Found by:
[package-2]
Version: 2.0.1
Type: deb
Found by:

View File

@ -1,4 +1,4 @@
package text package imgs
import ( import (
"fmt" "fmt"
@ -9,13 +9,11 @@ import (
stereoscopeImg "github.com/anchore/stereoscope/pkg/image" stereoscopeImg "github.com/anchore/stereoscope/pkg/image"
) )
// Presenter holds the Present method to produce output
type Presenter struct { type Presenter struct {
img *stereoscopeImg.Image img *stereoscopeImg.Image
catalog *pkg.Catalog catalog *pkg.Catalog
} }
// NewPresenter is a constructor for a Presenter
func NewPresenter(img *stereoscopeImg.Image, catalog *pkg.Catalog) *Presenter { func NewPresenter(img *stereoscopeImg.Image, catalog *pkg.Catalog) *Presenter {
return &Presenter{ return &Presenter{
img: img, img: img,
@ -46,6 +44,7 @@ func (pres *Presenter) Present(output io.Writer) error {
} }
// populate artifacts... // populate artifacts...
// TODO: move this into a common package so that other text presenters can reuse
for p := range pres.catalog.Enumerate() { for p := range pres.catalog.Enumerate() {
fmt.Fprintln(w, fmt.Sprintf("[%s]", p.Name)) fmt.Fprintln(w, fmt.Sprintf("[%s]", p.Name))
fmt.Fprintln(w, " Version:\t", p.Version) fmt.Fprintln(w, " Version:\t", p.Version)

View File

@ -1,4 +1,4 @@
package text package imgs
import ( import (
"bytes" "bytes"

58
imgbom/protocol.go Normal file
View File

@ -0,0 +1,58 @@
package imgbom
import "strings"
// Potentially consider moving this out into a generic package that parses user input.
// Aside from scope, this is the 2nd package that looks at a string to parse the input
// and return an Option type.
const (
UnknownProtocol ProtocolType = iota
ImageProtocol
DirProtocol
)
var optionStr = []string{
"UnknownProtocol",
"image",
"dir",
}
type ProtocolType int
type Protocol struct {
Type ProtocolType
Value string
}
func NewProtocol(userStr string) Protocol {
candidates := strings.Split(userStr, "://")
switch len(candidates) {
case 2:
if strings.HasPrefix(userStr, "dir://") {
return Protocol{
Type: DirProtocol,
Value: strings.TrimPrefix(userStr, "dir://"),
}
}
// default to an Image for anything else since stereoscope can handle this
return Protocol{
Type: ImageProtocol,
Value: userStr,
}
default:
return Protocol{
Type: ImageProtocol,
Value: userStr,
}
}
}
func (o ProtocolType) String() string {
if int(o) >= len(optionStr) || o < 0 {
return optionStr[0]
}
return optionStr[o]
}

43
imgbom/protocol_test.go Normal file
View File

@ -0,0 +1,43 @@
package imgbom
import "testing"
func TestNewProtocol(t *testing.T) {
testCases := []struct {
desc string
input string
expType ProtocolType
expValue string
}{
{
desc: "directory protocol",
input: "dir:///opt/",
expType: DirProtocol,
expValue: "/opt/",
},
{
desc: "unknown protocol",
input: "s4:///opt/",
expType: ImageProtocol,
expValue: "s4:///opt/",
},
{
desc: "docker protocol",
input: "docker://ubuntu:20.04",
expType: ImageProtocol,
expValue: "docker://ubuntu:20.04",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
p := NewProtocol(test.input)
if p.Type != test.expType {
t.Errorf("mismatched type in protocol: '%v' != '%v'", p.Type, test.expType)
}
if p.Value != test.expValue {
t.Errorf("mismatched protocol value: '%s' != '%s'", p.Value, test.expValue)
}
})
}
}

View File

@ -8,6 +8,15 @@ import (
"github.com/anchore/stereoscope/pkg/image" "github.com/anchore/stereoscope/pkg/image"
) )
type FileContentResolver interface {
ContentResolver
FileResolver
}
type ContentResolver interface {
MultipleFileContentsByRef(f ...file.Reference) (map[file.Reference]string, error)
}
type FileResolver interface { type FileResolver interface {
FilesByPath(paths ...file.Path) ([]file.Reference, error) FilesByPath(paths ...file.Path) ([]file.Reference, error)
FilesByGlob(patterns ...string) ([]file.Reference, error) FilesByGlob(patterns ...string) ([]file.Reference, error)

View File

@ -2,38 +2,127 @@ package scope
import ( import (
"fmt" "fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"github.com/anchore/imgbom/internal/log"
"github.com/anchore/stereoscope/pkg/file" "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/image" "github.com/anchore/stereoscope/pkg/image"
) )
type Scope struct { type DirectoryScope struct {
Option Option
Path string
}
func (s DirectoryScope) String() string {
return fmt.Sprintf("dir://%s", s.Path)
}
func (s DirectoryScope) 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 DirectoryScope) 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 DirectoryScope) 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
}
type ImageScope struct {
Option Option Option Option
resolver FileResolver resolver FileResolver
Image *image.Image Image *image.Image
} }
func NewScope(img *image.Image, option Option) (Scope, error) { func NewDirScope(path string, option Option) (DirectoryScope, error) {
return DirectoryScope{
Option: option,
Path: path,
}, nil
}
func NewImageScope(img *image.Image, option Option) (ImageScope, error) {
if img == nil { if img == nil {
return Scope{}, fmt.Errorf("no image given") return ImageScope{}, fmt.Errorf("no image given")
} }
resolver, err := getFileResolver(img, option) resolver, err := getFileResolver(img, option)
if err != nil { if err != nil {
return Scope{}, fmt.Errorf("could not determine file resolver: %w", err) return ImageScope{}, fmt.Errorf("could not determine file resolver: %w", err)
} }
return Scope{ return ImageScope{
Option: option, Option: option,
resolver: resolver, resolver: resolver,
Image: img, Image: img,
}, nil }, nil
} }
func (s Scope) FilesByPath(paths ...file.Path) ([]file.Reference, error) { func (s ImageScope) FilesByPath(paths ...file.Path) ([]file.Reference, error) {
return s.resolver.FilesByPath(paths...) return s.resolver.FilesByPath(paths...)
} }
func (s Scope) FilesByGlob(patterns ...string) ([]file.Reference, error) { func (s ImageScope) FilesByGlob(patterns ...string) ([]file.Reference, error) {
return s.resolver.FilesByGlob(patterns...) return s.resolver.FilesByGlob(patterns...)
} }
func (s ImageScope) MultipleFileContentsByRef(f ...file.Reference) (map[file.Reference]string, error) {
return s.Image.MultipleFileContentsByRef(f...)
}

147
imgbom/scope/scope_test.go Normal file
View File

@ -0,0 +1,147 @@
package scope
import (
"testing"
"github.com/anchore/stereoscope/pkg/file"
)
func TestDirectoryScope(t *testing.T) {
testCases := []struct {
desc string
input string
expString string
inputPaths []file.Path
expRefs int
}{
{
desc: "no paths exist",
input: "foobar/",
expString: "dir://foobar/",
inputPaths: []file.Path{file.Path("/opt/"), file.Path("/other")},
expRefs: 0,
},
{
desc: "path detected",
input: "test-fixtures",
expString: "dir://test-fixtures",
inputPaths: []file.Path{file.Path("path-detected")},
expRefs: 1,
},
{
desc: "no files-by-path detected",
input: "test-fixtures",
expString: "dir://test-fixtures",
inputPaths: []file.Path{file.Path("no-path-detected")},
expRefs: 0,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
p, err := NewDirScope(test.input, AllLayersScope)
if err != nil {
t.Errorf("could not create NewDirScope: %w", err)
}
if p.String() != test.expString {
t.Errorf("mismatched stringer: '%s' != '%s'", p.String(), test.expString)
}
refs, err := p.FilesByPath(test.inputPaths...)
if err != nil {
t.Errorf("FilesByPath call produced an error: %w", err)
}
if len(refs) != test.expRefs {
t.Errorf("unexpected number of refs returned: %d != %d", len(refs), test.expRefs)
}
})
}
}
func TestMultipleFileContentsByRef(t *testing.T) {
testCases := []struct {
desc string
input string
path string
expected string
}{
{
input: "test-fixtures/path-detected",
desc: "empty file",
path: "empty",
expected: "",
},
{
input: "test-fixtures/path-detected",
desc: "path does not exist",
path: "foo",
expected: "",
},
{
input: "test-fixtures/path-detected",
desc: "file has contents",
path: "test-fixtures/path-detected/.vimrc",
expected: "\" A .vimrc file\n",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
p, err := NewDirScope(test.input, AllLayersScope)
if err != nil {
t.Errorf("could not create NewDirScope: %w", err)
}
ref := file.NewFileReference(file.Path(test.path))
contents, err := p.MultipleFileContentsByRef(ref)
content := contents[ref]
if content != test.expected {
t.Errorf("unexpected contents from file: '%s' != '%s'", content, test.expected)
}
})
}
}
func TestFilesByGlob(t *testing.T) {
testCases := []struct {
desc string
input string
glob string
expected int
}{
{
input: "test-fixtures",
desc: "no matches",
glob: "bar/foo",
expected: 0,
},
{
input: "test-fixtures/path-detected",
desc: "a single match",
glob: "*vimrc",
expected: 1,
},
{
input: "test-fixtures/path-detected",
desc: "multiple matches",
glob: "*",
expected: 2,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
p, err := NewDirScope(test.input, AllLayersScope)
if err != nil {
t.Errorf("could not create NewDirScope: %w", err)
}
contents, err := p.FilesByGlob(test.glob)
if len(contents) != test.expected {
t.Errorf("unexpected number of files found by glob (%s): %d != %d", test.glob, len(contents), test.expected)
}
})
}
}

View File

@ -0,0 +1 @@
" A .vimrc file

View File

@ -0,0 +1,77 @@
// +build integration
package integration
import (
"bytes"
"flag"
"testing"
"github.com/anchore/go-testutils"
"github.com/anchore/imgbom/imgbom"
"github.com/anchore/imgbom/imgbom/presenter"
"github.com/anchore/imgbom/imgbom/scope"
"github.com/sergi/go-diff/diffmatchpatch"
)
var update = flag.Bool("update", false, "update the *.golden files for json presenters")
func TestDirTextPresenter(t *testing.T) {
var buffer bytes.Buffer
protocol := imgbom.NewProtocol("dir://test-fixtures")
if protocol.Type != imgbom.DirProtocol {
t.Errorf("unexpected protocol returned: %v != %v", protocol.Type, imgbom.DirProtocol)
}
catalog, err := imgbom.CatalogDir(protocol.Value, scope.AllLayersScope)
if err != nil {
t.Errorf("could not produce catalog: %w", err)
}
presenterOpt := presenter.ParseOption("text")
dirPresenter := presenter.GetDirPresenter(presenterOpt, protocol.Value, catalog)
dirPresenter.Present(&buffer)
actual := buffer.Bytes()
if *update {
testutils.UpdateGoldenFileContents(t, actual)
}
var expected = testutils.GetGoldenFileContents(t)
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(actual), string(expected), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
}
func TestDirJsonPresenter(t *testing.T) {
var buffer bytes.Buffer
protocol := imgbom.NewProtocol("dir://test-fixtures")
if protocol.Type != imgbom.DirProtocol {
t.Errorf("unexpected protocol returned: %v != %v", protocol.Type, imgbom.DirProtocol)
}
catalog, err := imgbom.CatalogDir(protocol.Value, scope.AllLayersScope)
if err != nil {
t.Errorf("could not produce catalog: %w", err)
}
presenterOpt := presenter.ParseOption("json")
dirPresenter := presenter.GetDirPresenter(presenterOpt, protocol.Value, catalog)
dirPresenter.Present(&buffer)
actual := buffer.Bytes()
if *update {
testutils.UpdateGoldenFileContents(t, actual)
}
var expected = testutils.GetGoldenFileContents(t)
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(actual), string(expected), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
}

View File

@ -6,7 +6,6 @@ import (
"testing" "testing"
"github.com/anchore/go-testutils" "github.com/anchore/go-testutils"
"github.com/anchore/imgbom/imgbom"
"github.com/anchore/imgbom/imgbom/cataloger" "github.com/anchore/imgbom/imgbom/cataloger"
"github.com/anchore/imgbom/imgbom/pkg" "github.com/anchore/imgbom/imgbom/pkg"
"github.com/anchore/imgbom/imgbom/scope" "github.com/anchore/imgbom/imgbom/scope"
@ -16,7 +15,8 @@ func TestLanguageImage(t *testing.T) {
img, cleanup := testutils.GetFixtureImage(t, "docker-archive", "image-language-pkgs") img, cleanup := testutils.GetFixtureImage(t, "docker-archive", "image-language-pkgs")
defer cleanup() defer cleanup()
catalog, err := imgbom.CatalogImage(img, scope.AllLayersScope) s, err := scope.NewImageScope(img, scope.AllLayersScope)
catalog, err := cataloger.Catalog(s)
if err != nil { if err != nil {
t.Fatalf("failed to catalog image: %+v", err) t.Fatalf("failed to catalog image: %+v", err)
} }

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,256 @@
[Path: test-fixtures]
[actionmailer]
Version: 4.1.1
Type: bundle
Found by: bundler-cataloger
[actionpack]
Version: 4.1.1
Type: bundle
Found by: bundler-cataloger
[actionview]
Version: 4.1.1
Type: bundle
Found by: bundler-cataloger
[activemodel]
Version: 4.1.1
Type: bundle
Found by: bundler-cataloger
[activerecord]
Version: 4.1.1
Type: bundle
Found by: bundler-cataloger
[activesupport]
Version: 4.1.1
Type: bundle
Found by: bundler-cataloger
[arel]
Version: 5.0.1.20140414130214
Type: bundle
Found by: bundler-cataloger
[bootstrap-sass]
Version: 3.1.1.1
Type: bundle
Found by: bundler-cataloger
[builder]
Version: 3.2.2
Type: bundle
Found by: bundler-cataloger
[coffee-rails]
Version: 4.0.1
Type: bundle
Found by: bundler-cataloger
[coffee-script]
Version: 2.2.0
Type: bundle
Found by: bundler-cataloger
[coffee-script-source]
Version: 1.7.0
Type: bundle
Found by: bundler-cataloger
[erubis]
Version: 2.7.0
Type: bundle
Found by: bundler-cataloger
[execjs]
Version: 2.0.2
Type: bundle
Found by: bundler-cataloger
[hike]
Version: 1.2.3
Type: bundle
Found by: bundler-cataloger
[i18n]
Version: 0.6.9
Type: bundle
Found by: bundler-cataloger
[jbuilder]
Version: 2.0.7
Type: bundle
Found by: bundler-cataloger
[jquery-rails]
Version: 3.1.0
Type: bundle
Found by: bundler-cataloger
[json]
Version: 1.8.1
Type: bundle
Found by: bundler-cataloger
[kgio]
Version: 2.9.2
Type: bundle
Found by: bundler-cataloger
[libv8]
Version: 3.16.14.3
Type: bundle
Found by: bundler-cataloger
[mail]
Version: 2.5.4
Type: bundle
Found by: bundler-cataloger
[mime-types]
Version: 1.25.1
Type: bundle
Found by: bundler-cataloger
[minitest]
Version: 5.3.4
Type: bundle
Found by: bundler-cataloger
[multi_json]
Version: 1.10.1
Type: bundle
Found by: bundler-cataloger
[mysql2]
Version: 0.3.16
Type: bundle
Found by: bundler-cataloger
[polyglot]
Version: 0.3.4
Type: bundle
Found by: bundler-cataloger
[rack]
Version: 1.5.2
Type: bundle
Found by: bundler-cataloger
[rack-test]
Version: 0.6.2
Type: bundle
Found by: bundler-cataloger
[rails]
Version: 4.1.1
Type: bundle
Found by: bundler-cataloger
[railties]
Version: 4.1.1
Type: bundle
Found by: bundler-cataloger
[raindrops]
Version: 0.13.0
Type: bundle
Found by: bundler-cataloger
[rake]
Version: 10.3.2
Type: bundle
Found by: bundler-cataloger
[rdoc]
Version: 4.1.1
Type: bundle
Found by: bundler-cataloger
[ref]
Version: 1.0.5
Type: bundle
Found by: bundler-cataloger
[sass]
Version: 3.2.19
Type: bundle
Found by: bundler-cataloger
[sass-rails]
Version: 4.0.3
Type: bundle
Found by: bundler-cataloger
[sdoc]
Version: 0.4.0
Type: bundle
Found by: bundler-cataloger
[spring]
Version: 1.1.3
Type: bundle
Found by: bundler-cataloger
[sprockets]
Version: 2.11.0
Type: bundle
Found by: bundler-cataloger
[sprockets-rails]
Version: 2.1.3
Type: bundle
Found by: bundler-cataloger
[sqlite3]
Version: 1.3.9
Type: bundle
Found by: bundler-cataloger
[therubyracer]
Version: 0.12.1
Type: bundle
Found by: bundler-cataloger
[thor]
Version: 0.19.1
Type: bundle
Found by: bundler-cataloger
[thread_safe]
Version: 0.3.3
Type: bundle
Found by: bundler-cataloger
[tilt]
Version: 1.4.1
Type: bundle
Found by: bundler-cataloger
[treetop]
Version: 1.4.15
Type: bundle
Found by: bundler-cataloger
[turbolinks]
Version: 2.2.2
Type: bundle
Found by: bundler-cataloger
[tzinfo]
Version: 1.2.0
Type: bundle
Found by: bundler-cataloger
[uglifier]
Version: 2.5.0
Type: bundle
Found by: bundler-cataloger
[unicorn]
Version: 4.8.3
Type: bundle
Found by: bundler-cataloger