From 4d31655908099326f17e39a88381546ec25754e2 Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Mon, 29 Jun 2020 11:34:00 -0400 Subject: [PATCH 01/19] allow root command to catalog a directory Signed-off-by: Alfredo Deza --- cmd/root.go | 58 +++++++------- imgbom/presenter/json/presenter.go | 119 ----------------------------- 2 files changed, 30 insertions(+), 147 deletions(-) delete mode 100644 imgbom/presenter/json/presenter.go diff --git a/cmd/root.go b/cmd/root.go index 94534dbe6..fbd811e33 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -17,13 +17,14 @@ import ( ) var rootCmd = &cobra.Command{ - Use: fmt.Sprintf("%s [IMAGE]", internal.ApplicationName), - Short: "A container image BOM tool", // TODO: add copy + Use: fmt.Sprintf("%s [SOURCE]", internal.ApplicationName), + Short: "A tool that generates a Software Build Of Materials (SBOM)", Long: internal.Tprintf(`\ Supports the following image sources: {{.appName}} yourrepo/yourimage:tag defaults to using images from a 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{}{ "appName": internal.ApplicationName, }), @@ -44,38 +45,38 @@ func init() { ) } -func startWorker(userImage string) <-chan error { +func startWorker(userInput string) <-chan error { errs := make(chan error) go func() { defer close(errs) - - log.Infof("Fetching image '%s'", userImage) - img, err := stereoscope.GetImage(userImage) - if err != nil { - errs <- fmt.Errorf("could not fetch image '%s': %w", userImage, err) - 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.CatalogImage(img, appConfig.ScopeOpt) + protocol := imgbom.NewProtocol(userInput) + fmt.Printf("protocol: %+v", protocol) + catalog, err := imgbom.Catalog(protocol, 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), - }) + switch protocol.Type { + case imgbom.DirProtocol: + log.Info("Cataloging directory") + bus.Publish(partybus.Event{ + Type: event.CatalogerFinished, + Value: presenter.GetDirPresenter(appConfig.PresenterOpt, catalog), + }) + default: + log.Info("Cataloging image") + log.Infof("Fetching image '%s'", userInput) + img, err := stereoscope.GetImage(userInput) + if err != nil { + errs <- fmt.Errorf("could not fetch image '%s': %w", userInput, err) + } + defer stereoscope.Cleanup() + + bus.Publish(partybus.Event{ + Type: event.CatalogerFinished, + Value: presenter.GetImgPresenter(appConfig.PresenterOpt, img, catalog), + }) + } }() return errs } @@ -86,4 +87,5 @@ func doRunCmd(_ *cobra.Command, args []string) int { ux := ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet) return ux(errs, eventSubscription) + } diff --git a/imgbom/presenter/json/presenter.go b/imgbom/presenter/json/presenter.go deleted file mode 100644 index 995b36f98..000000000 --- a/imgbom/presenter/json/presenter.go +++ /dev/null @@ -1,119 +0,0 @@ -package json - -import ( - "encoding/json" - "io" - - "github.com/anchore/imgbom/imgbom/pkg" - "github.com/anchore/imgbom/internal/log" - stereoscopeImg "github.com/anchore/stereoscope/pkg/image" -) - -type Presenter struct { - img *stereoscopeImg.Image - catalog *pkg.Catalog -} - -func NewPresenter(img *stereoscopeImg.Image, catalog *pkg.Catalog) *Presenter { - return &Presenter{ - img: img, - catalog: catalog, - } -} - -type document struct { - Image image `json:"image"` - Artifacts []artifact `json:"artifacts"` -} - -type image struct { - Layers []layer `json:"layers"` - Size int64 `json:"size"` - Digest string `json:"digest"` - MediaType string `json:"mediaType"` - Tags []string `json:"tags"` -} - -type layer struct { - MediaType string `json:"mediaType"` - Digest string `json:"digest"` - Size int64 `json:"size"` -} - -type source struct { - FoundBy string `json:"foundBy"` - Layer int `json:"layer"` - 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 { - tags := make([]string, len(pres.img.Metadata.Tags)) - for idx, tag := range pres.img.Metadata.Tags { - tags[idx] = tag.String() - } - - doc := document{ - Image: image{ - Digest: pres.img.Metadata.Digest, - Size: pres.img.Metadata.Size, - MediaType: string(pres.img.Metadata.MediaType), - Tags: tags, - Layers: make([]layer, len(pres.img.Layers)), - }, - Artifacts: make([]artifact, 0), - } - - // populate image... - for idx, l := range pres.img.Layers { - doc.Image.Layers[idx] = layer{ - MediaType: string(l.Metadata.MediaType), - Digest: l.Metadata.Digest, - Size: l.Metadata.Size, - } - } - - // populate artifacts... - 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, src := range p.Source { - fileMetadata, err := pres.img.FileCatalog.Get(src) - if err != nil { - // TODO: test case - log.Errorf("could not get metadata from catalog (presenter=json): %+v", src) - } - - srcObj := source{ - FoundBy: p.FoundBy, - Layer: int(fileMetadata.Source.Metadata.Index), - 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 -} From 678a2c1dcb887a44343a3f3a3f14ae7d314680d7 Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Mon, 29 Jun 2020 11:35:43 -0400 Subject: [PATCH 02/19] create a protocol-parsing package to distinguish dir,docker, etc... Signed-off-by: Alfredo Deza --- imgbom/protocol.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 imgbom/protocol.go diff --git a/imgbom/protocol.go b/imgbom/protocol.go new file mode 100644 index 000000000..41b67258d --- /dev/null +++ b/imgbom/protocol.go @@ -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] +} From faf21a67a68cabfd4f15eb667b03fba6648cc5ab Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Mon, 29 Jun 2020 11:38:29 -0400 Subject: [PATCH 03/19] lib: return a protocol-based scope (dir or img) Signed-off-by: Alfredo Deza --- imgbom/lib.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/imgbom/lib.go b/imgbom/lib.go index d0368a2f5..c0e6a4b20 100644 --- a/imgbom/lib.go +++ b/imgbom/lib.go @@ -16,12 +16,19 @@ func IdentifyDistro(img *image.Image) *distro.Distro { return distro.Identify(img) } -func CatalogImage(img *image.Image, o scope.Option) (*pkg.Catalog, error) { - s, err := scope.NewScope(img, o) +func CatalogDir(d string, o scope.Option) (*pkg.Catalog, error) { + s, err := scope.NewDirScope(d, o) if err != nil { 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) } From 507a6b69cb7269330c7547ca08c1d21cceac9c1d Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Mon, 29 Jun 2020 11:44:03 -0400 Subject: [PATCH 04/19] catalog controller uses scope to determine file resolvers Signed-off-by: Alfredo Deza --- imgbom/cataloger/controller.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/imgbom/cataloger/controller.go b/imgbom/cataloger/controller.go index cf4197453..294a13b29 100644 --- a/imgbom/cataloger/controller.go +++ b/imgbom/cataloger/controller.go @@ -29,7 +29,7 @@ func Catalogers() []string { return c } -func Catalog(s scope.Scope) (*pkg.Catalog, error) { +func Catalog(s scope.FileContentResolver) (*pkg.Catalog, error) { return controllerInstance.catalog(s) } @@ -71,7 +71,7 @@ func (c *controller) trackCataloger() (*progress.Manual, *progress.Manual) { 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() 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 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)) filesProcessed.N += int64(len(fileSelection)) } // fetch contents for requested selection by catalogers - contents, err := s.Image.MultipleFileContentsByRef(fileSelection...) + contents, err := s.MultipleFileContentsByRef(fileSelection...) if err != nil { return nil, err } From 69a0376f99ef7e0605544b6d47c33503c7e25ce7 Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Mon, 29 Jun 2020 11:45:43 -0400 Subject: [PATCH 05/19] distro: add note to replace with MultipleFileContents in the future Signed-off-by: Alfredo Deza --- imgbom/distro/identify.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imgbom/distro/identify.go b/imgbom/distro/identify.go index 3249c01cf..ccabd0819 100644 --- a/imgbom/distro/identify.go +++ b/imgbom/distro/identify.go @@ -24,7 +24,7 @@ func Identify(img *image.Image) *Distro { } for path, fn := range identityFiles { - contents, err := img.FileContentsFromSquash(path) + contents, err := img.FileContentsFromSquash(path) // TODO: this call replaced with "MultipleFileContents" if err != nil { log.Debugf("unable to get contents from %s: %s", path, err) From 312c8acfbeef7d3e9362b982a5bb52a3f39df639 Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Mon, 29 Jun 2020 12:00:27 -0400 Subject: [PATCH 06/19] presenter can pick text-dir, text-img, json-text, or json-img for reporting Signed-off-by: Alfredo Deza --- imgbom/presenter/presenter.go | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/imgbom/presenter/presenter.go b/imgbom/presenter/presenter.go index 6a51c5e74..5170c6d57 100644 --- a/imgbom/presenter/presenter.go +++ b/imgbom/presenter/presenter.go @@ -4,8 +4,10 @@ import ( "io" "github.com/anchore/imgbom/imgbom/pkg" - "github.com/anchore/imgbom/imgbom/presenter/json" - "github.com/anchore/imgbom/imgbom/presenter/text" + json_dirs "github.com/anchore/imgbom/imgbom/presenter/json/dirs" + 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" ) @@ -13,13 +15,23 @@ type Presenter interface { 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 { case JSONPresenter: - return json.NewPresenter(img, catalog) + return json_imgs.NewPresenter(img, catalog) 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: return nil } From 268cfef0ee9679a6c3cec1810ea164374a1130a5 Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Mon, 29 Jun 2020 13:38:15 -0400 Subject: [PATCH 07/19] re-organize json-based presenters for dirs and imgs Signed-off-by: Alfredo Deza --- imgbom/presenter/json/dirs/presenter.go | 85 +++++++++++++++++ imgbom/presenter/json/imgs/presenter.go | 119 ++++++++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 imgbom/presenter/json/dirs/presenter.go create mode 100644 imgbom/presenter/json/imgs/presenter.go diff --git a/imgbom/presenter/json/dirs/presenter.go b/imgbom/presenter/json/dirs/presenter.go new file mode 100644 index 000000000..2707f0a91 --- /dev/null +++ b/imgbom/presenter/json/dirs/presenter.go @@ -0,0 +1,85 @@ +package dirs + +import ( + "encoding/json" + "io" + + "github.com/anchore/imgbom/imgbom/pkg" + "github.com/anchore/imgbom/internal/log" +) + +type Presenter struct { + catalog *pkg.Catalog +} + +func NewPresenter(catalog *pkg.Catalog) *Presenter { + return &Presenter{ + catalog: catalog, + } +} + +type document struct { + Artifacts []artifact `json:"artifacts"` +} + +type dir struct { + Path string `json:"path"` +} + +type source struct { + FoundBy string `json:"foundBy"` + Layer int `json:"layer"` + 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), + } + + // populate artifacts... + 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, + } + + // FIXME: there is no image in a dir-based scan + for idx := range p.Source { + // fileMetadata, err := pres.img.FileCatalog.Get(src) + // if err != nil { + // // TODO: test case + // log.Errorf("could not get metadata from catalog (presenter=json): %+v", src) + // } + + srcObj := source{ + FoundBy: "FoundBy", + Layer: 0, + 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 +} diff --git a/imgbom/presenter/json/imgs/presenter.go b/imgbom/presenter/json/imgs/presenter.go new file mode 100644 index 000000000..fdb259289 --- /dev/null +++ b/imgbom/presenter/json/imgs/presenter.go @@ -0,0 +1,119 @@ +package imgs + +import ( + "encoding/json" + "io" + + "github.com/anchore/imgbom/imgbom/pkg" + "github.com/anchore/imgbom/internal/log" + stereoscopeImg "github.com/anchore/stereoscope/pkg/image" +) + +type Presenter struct { + img *stereoscopeImg.Image + catalog *pkg.Catalog +} + +func NewPresenter(img *stereoscopeImg.Image, catalog *pkg.Catalog) *Presenter { + return &Presenter{ + img: img, + catalog: catalog, + } +} + +type document struct { + Image image `json:"image"` + Artifacts []artifact `json:"artifacts"` +} + +type image struct { + Layers []layer `json:"layers"` + Size int64 `json:"size"` + Digest string `json:"digest"` + MediaType string `json:"mediaType"` + Tags []string `json:"tags"` +} + +type layer struct { + MediaType string `json:"mediaType"` + Digest string `json:"digest"` + Size int64 `json:"size"` +} + +type source struct { + FoundBy string `json:"foundBy"` + Layer int `json:"layer"` + 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 { + tags := make([]string, len(pres.img.Metadata.Tags)) + for idx, tag := range pres.img.Metadata.Tags { + tags[idx] = tag.String() + } + + doc := document{ + Image: image{ + Digest: pres.img.Metadata.Digest, + Size: pres.img.Metadata.Size, + MediaType: string(pres.img.Metadata.MediaType), + Tags: tags, + Layers: make([]layer, len(pres.img.Layers)), + }, + Artifacts: make([]artifact, 0), + } + + // populate image... + for idx, l := range pres.img.Layers { + doc.Image.Layers[idx] = layer{ + MediaType: string(l.Metadata.MediaType), + Digest: l.Metadata.Digest, + Size: l.Metadata.Size, + } + } + + // populate artifacts... + 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, src := range p.Source { + fileMetadata, err := pres.img.FileCatalog.Get(src) + if err != nil { + // TODO: test case + log.Errorf("could not get metadata from catalog (presenter=json): %+v", src) + } + + srcObj := source{ + FoundBy: p.FoundBy, + Layer: int(fileMetadata.Source.Metadata.Index), + 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 +} From e38e3e9ff489a721cf6bf9df4b46ae241f2ad07c Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Mon, 29 Jun 2020 13:38:54 -0400 Subject: [PATCH 08/19] create text-based presenters for dirs and imgs Signed-off-by: Alfredo Deza --- imgbom/presenter/json/dirs/presenter.go | 22 +++---- imgbom/presenter/text/dirs/presenter.go | 45 ++++++++++++++ imgbom/presenter/text/dirs/presenter_test.go | 53 ++++++++++++++++ .../snapshot/TestJsonPresenter.golden | 1 + .../snapshot/TestTextPresenter.golden | 11 ++++ imgbom/presenter/text/imgs/presenter.go | 61 +++++++++++++++++++ imgbom/presenter/text/presenter_test.go | 2 +- 7 files changed, 179 insertions(+), 16 deletions(-) create mode 100644 imgbom/presenter/text/dirs/presenter.go create mode 100644 imgbom/presenter/text/dirs/presenter_test.go create mode 100644 imgbom/presenter/text/dirs/test-fixtures/snapshot/TestJsonPresenter.golden create mode 100644 imgbom/presenter/text/dirs/test-fixtures/snapshot/TestTextPresenter.golden create mode 100644 imgbom/presenter/text/imgs/presenter.go diff --git a/imgbom/presenter/json/dirs/presenter.go b/imgbom/presenter/json/dirs/presenter.go index 2707f0a91..25e4d15a6 100644 --- a/imgbom/presenter/json/dirs/presenter.go +++ b/imgbom/presenter/json/dirs/presenter.go @@ -10,25 +10,23 @@ import ( type Presenter struct { catalog *pkg.Catalog + path string } -func NewPresenter(catalog *pkg.Catalog) *Presenter { +func NewPresenter(catalog *pkg.Catalog, path string) *Presenter { return &Presenter{ catalog: catalog, + path: path, } } type document struct { Artifacts []artifact `json:"artifacts"` -} - -type dir struct { - Path string `json:"path"` + Source string } type source struct { FoundBy string `json:"foundBy"` - Layer int `json:"layer"` Effects []string `json:"effects"` } @@ -44,9 +42,11 @@ type artifact struct { 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, @@ -56,17 +56,9 @@ func (pres *Presenter) Present(output io.Writer) error { Metadata: p.Metadata, } - // FIXME: there is no image in a dir-based scan for idx := range p.Source { - // fileMetadata, err := pres.img.FileCatalog.Get(src) - // if err != nil { - // // TODO: test case - // log.Errorf("could not get metadata from catalog (presenter=json): %+v", src) - // } - srcObj := source{ - FoundBy: "FoundBy", - Layer: 0, + FoundBy: p.FoundBy, Effects: []string{}, // TODO } art.Sources[idx] = srcObj diff --git a/imgbom/presenter/text/dirs/presenter.go b/imgbom/presenter/text/dirs/presenter.go new file mode 100644 index 000000000..8040b337c --- /dev/null +++ b/imgbom/presenter/text/dirs/presenter.go @@ -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 +} diff --git a/imgbom/presenter/text/dirs/presenter_test.go b/imgbom/presenter/text/dirs/presenter_test.go new file mode 100644 index 000000000..20e9a62f8 --- /dev/null +++ b/imgbom/presenter/text/dirs/presenter_test.go @@ -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)) + } + +} diff --git a/imgbom/presenter/text/dirs/test-fixtures/snapshot/TestJsonPresenter.golden b/imgbom/presenter/text/dirs/test-fixtures/snapshot/TestJsonPresenter.golden new file mode 100644 index 000000000..ab2fd8317 --- /dev/null +++ b/imgbom/presenter/text/dirs/test-fixtures/snapshot/TestJsonPresenter.golden @@ -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"} \ No newline at end of file diff --git a/imgbom/presenter/text/dirs/test-fixtures/snapshot/TestTextPresenter.golden b/imgbom/presenter/text/dirs/test-fixtures/snapshot/TestTextPresenter.golden new file mode 100644 index 000000000..f41bf461d --- /dev/null +++ b/imgbom/presenter/text/dirs/test-fixtures/snapshot/TestTextPresenter.golden @@ -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: + diff --git a/imgbom/presenter/text/imgs/presenter.go b/imgbom/presenter/text/imgs/presenter.go new file mode 100644 index 000000000..6a51b2186 --- /dev/null +++ b/imgbom/presenter/text/imgs/presenter.go @@ -0,0 +1,61 @@ +package imgs + +import ( + "fmt" + "io" + "text/tabwriter" + + "github.com/anchore/imgbom/imgbom/pkg" + stereoscopeImg "github.com/anchore/stereoscope/pkg/image" +) + +type Presenter struct { + img *stereoscopeImg.Image + catalog *pkg.Catalog +} + +func NewPresenter(img *stereoscopeImg.Image, catalog *pkg.Catalog) *Presenter { + return &Presenter{ + img: img, + catalog: catalog, + } +} + +// Present is a method that is in charge of writing to an output buffer +func (pres *Presenter) Present(output io.Writer) error { + tags := make([]string, len(pres.img.Metadata.Tags)) + for idx, tag := range pres.img.Metadata.Tags { + tags[idx] = tag.String() + } + + // init the tabular writer + w := new(tabwriter.Writer) + w.Init(output, 0, 8, 0, '\t', tabwriter.AlignRight) + + fmt.Fprintln(w, "[Image]") + + for idx, l := range pres.img.Layers { + fmt.Fprintln(w, " Layer:\t", idx) + fmt.Fprintln(w, " Digest:\t", l.Metadata.Digest) + fmt.Fprintln(w, " Size:\t", l.Metadata.Size) + fmt.Fprintln(w, " MediaType:\t", l.Metadata.MediaType) + fmt.Fprintln(w) + w.Flush() + } + + // 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 +} diff --git a/imgbom/presenter/text/presenter_test.go b/imgbom/presenter/text/presenter_test.go index 9b3feaeab..ef0b0e75a 100644 --- a/imgbom/presenter/text/presenter_test.go +++ b/imgbom/presenter/text/presenter_test.go @@ -1,4 +1,4 @@ -package text +package imgs import ( "bytes" From b2d7bd3b1a78d36db4e401edbf2e07abeec35df6 Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Mon, 29 Jun 2020 13:41:20 -0400 Subject: [PATCH 09/19] scope: add directory-based scoping Signed-off-by: Alfredo Deza --- imgbom/scope/scope.go | 103 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 96 insertions(+), 7 deletions(-) diff --git a/imgbom/scope/scope.go b/imgbom/scope/scope.go index 623c9b77f..1dba71a5f 100644 --- a/imgbom/scope/scope.go +++ b/imgbom/scope/scope.go @@ -2,38 +2,127 @@ package scope import ( "fmt" + "io/ioutil" + "os" + "path" + "path/filepath" + "github.com/anchore/imgbom/internal/log" "github.com/anchore/stereoscope/pkg/file" "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 resolver FileResolver 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 { - return Scope{}, fmt.Errorf("no image given") + return ImageScope{}, fmt.Errorf("no image given") } resolver, err := getFileResolver(img, option) 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, resolver: resolver, Image: img, }, 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...) } -func (s Scope) FilesByGlob(patterns ...string) ([]file.Reference, error) { +func (s ImageScope) FilesByGlob(patterns ...string) ([]file.Reference, error) { return s.resolver.FilesByGlob(patterns...) } + +func (s ImageScope) MultipleFileContentsByRef(f ...file.Reference) (map[file.Reference]string, error) { + return s.Image.MultipleFileContentsByRef(f...) +} From ec52e9130166f244cd56637746e3b6d61649441f Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Mon, 29 Jun 2020 13:41:50 -0400 Subject: [PATCH 10/19] scope: create interface for content and file resolvers Signed-off-by: Alfredo Deza --- imgbom/scope/file_resolver.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/imgbom/scope/file_resolver.go b/imgbom/scope/file_resolver.go index 713fc28c4..7d8484de6 100644 --- a/imgbom/scope/file_resolver.go +++ b/imgbom/scope/file_resolver.go @@ -8,6 +8,15 @@ import ( "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 { FilesByPath(paths ...file.Path) ([]file.Reference, error) FilesByGlob(patterns ...string) ([]file.Reference, error) From feef7e28dc4bbe680b317c2f77564dffc45e6eaa Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Tue, 30 Jun 2020 09:24:22 -0400 Subject: [PATCH 11/19] cmd: add note about panic when img is nil Signed-off-by: Alfredo Deza --- cmd/root.go | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index fbd811e33..b5acd90a4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,8 +23,8 @@ var rootCmd = &cobra.Command{ Supports the following image sources: {{.appName}} yourrepo/yourimage:tag defaults to using images from a docker daemon {{.appName}} docker://yourrepo/yourimage:tag explicitly use the docker daemon - {{.appName}} tar://path/to/yourimage.tar use a tarball from disk - {{.appName}} dir://path/to/yourproject read directly from a path in 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{}{ "appName": internal.ApplicationName, }), @@ -51,27 +51,50 @@ func startWorker(userInput string) <-chan error { defer close(errs) protocol := imgbom.NewProtocol(userInput) fmt.Printf("protocol: %+v", protocol) - catalog, err := imgbom.Catalog(protocol, appConfig.ScopeOpt) - if err != nil { - errs <- fmt.Errorf("could not catalog image: %w", err) - } switch protocol.Type { case imgbom.DirProtocol: + log.Info("Cataloging directory") + 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, catalog), + Value: presenter.GetDirPresenter(appConfig.PresenterOpt, protocol.Value, catalog), }) default: - log.Info("Cataloging image") log.Infof("Fetching image '%s'", userInput) img, err := stereoscope.GetImage(userInput) - if err != nil { + + 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), @@ -87,5 +110,4 @@ func doRunCmd(_ *cobra.Command, args []string) int { ux := ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet) return ux(errs, eventSubscription) - } From 7544330bc2bbd4f81fbcc9866aa7b3e7fa5c9cbe Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Tue, 30 Jun 2020 09:25:25 -0400 Subject: [PATCH 12/19] json img presenter needs to degrade nicely when file metadata is not present Signed-off-by: Alfredo Deza --- imgbom/presenter/json/imgs/presenter.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/imgbom/presenter/json/imgs/presenter.go b/imgbom/presenter/json/imgs/presenter.go index fdb259289..8782a5afc 100644 --- a/imgbom/presenter/json/imgs/presenter.go +++ b/imgbom/presenter/json/imgs/presenter.go @@ -93,14 +93,18 @@ func (pres *Presenter) Present(output io.Writer) error { for idx, src := range p.Source { fileMetadata, err := pres.img.FileCatalog.Get(src) + var layer int if err != nil { // TODO: test case - log.Errorf("could not get metadata from catalog (presenter=json): %+v", src) + log.Errorf("could not get metadata from catalog (presenter=json): %+v - error: %w", src, err) + layer = 0 + } else { + layer = int(fileMetadata.Source.Metadata.Index) } srcObj := source{ FoundBy: p.FoundBy, - Layer: int(fileMetadata.Source.Metadata.Index), + Layer: layer, Effects: []string{}, // TODO } art.Sources[idx] = srcObj From 96d86fa2146d9fa18263ec467ff34403ea758536 Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Wed, 1 Jul 2020 10:35:09 -0400 Subject: [PATCH 13/19] tests: move existing json tests to json/imgs Signed-off-by: Alfredo Deza --- imgbom/presenter/json/imgs/presenter.go | 9 +++++---- imgbom/presenter/json/{ => imgs}/presenter_test.go | 2 +- .../test-fixtures/image-simple/Dockerfile | 0 .../test-fixtures/image-simple/file-1.txt | 0 .../test-fixtures/image-simple/file-2.txt | 0 .../image-simple/target/really/nested/file-3.txt | 0 .../test-fixtures/snapshot/TestJsonPresenter.golden | 0 .../snapshot/anchore-fixture-image-simple.golden | Bin 8 files changed, 6 insertions(+), 5 deletions(-) rename imgbom/presenter/json/{ => imgs}/presenter_test.go (99%) rename imgbom/presenter/json/{ => imgs}/test-fixtures/image-simple/Dockerfile (100%) rename imgbom/presenter/json/{ => imgs}/test-fixtures/image-simple/file-1.txt (100%) rename imgbom/presenter/json/{ => imgs}/test-fixtures/image-simple/file-2.txt (100%) rename imgbom/presenter/json/{ => imgs}/test-fixtures/image-simple/target/really/nested/file-3.txt (100%) rename imgbom/presenter/json/{ => imgs}/test-fixtures/snapshot/TestJsonPresenter.golden (100%) rename imgbom/presenter/json/{ => imgs}/test-fixtures/snapshot/anchore-fixture-image-simple.golden (100%) diff --git a/imgbom/presenter/json/imgs/presenter.go b/imgbom/presenter/json/imgs/presenter.go index 8782a5afc..7efaada71 100644 --- a/imgbom/presenter/json/imgs/presenter.go +++ b/imgbom/presenter/json/imgs/presenter.go @@ -2,6 +2,7 @@ package imgs import ( "encoding/json" + "fmt" "io" "github.com/anchore/imgbom/imgbom/pkg" @@ -82,6 +83,7 @@ func (pres *Presenter) Present(output io.Writer) error { } // 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, @@ -96,12 +98,11 @@ func (pres *Presenter) Present(output io.Writer) error { var layer int if err != nil { // TODO: test case - log.Errorf("could not get metadata from catalog (presenter=json): %+v - error: %w", src, err) - layer = 0 - } else { - layer = int(fileMetadata.Source.Metadata.Index) + return fmt.Errorf("could not get metadata from catalog (presenter=json src=%v): %w", src, err) } + layer = int(fileMetadata.Source.Metadata.Index) + srcObj := source{ FoundBy: p.FoundBy, Layer: layer, diff --git a/imgbom/presenter/json/presenter_test.go b/imgbom/presenter/json/imgs/presenter_test.go similarity index 99% rename from imgbom/presenter/json/presenter_test.go rename to imgbom/presenter/json/imgs/presenter_test.go index cb04a33ac..1f174bb1f 100644 --- a/imgbom/presenter/json/presenter_test.go +++ b/imgbom/presenter/json/imgs/presenter_test.go @@ -1,4 +1,4 @@ -package json +package imgs import ( "bytes" diff --git a/imgbom/presenter/json/test-fixtures/image-simple/Dockerfile b/imgbom/presenter/json/imgs/test-fixtures/image-simple/Dockerfile similarity index 100% rename from imgbom/presenter/json/test-fixtures/image-simple/Dockerfile rename to imgbom/presenter/json/imgs/test-fixtures/image-simple/Dockerfile diff --git a/imgbom/presenter/json/test-fixtures/image-simple/file-1.txt b/imgbom/presenter/json/imgs/test-fixtures/image-simple/file-1.txt similarity index 100% rename from imgbom/presenter/json/test-fixtures/image-simple/file-1.txt rename to imgbom/presenter/json/imgs/test-fixtures/image-simple/file-1.txt diff --git a/imgbom/presenter/json/test-fixtures/image-simple/file-2.txt b/imgbom/presenter/json/imgs/test-fixtures/image-simple/file-2.txt similarity index 100% rename from imgbom/presenter/json/test-fixtures/image-simple/file-2.txt rename to imgbom/presenter/json/imgs/test-fixtures/image-simple/file-2.txt diff --git a/imgbom/presenter/json/test-fixtures/image-simple/target/really/nested/file-3.txt b/imgbom/presenter/json/imgs/test-fixtures/image-simple/target/really/nested/file-3.txt similarity index 100% rename from imgbom/presenter/json/test-fixtures/image-simple/target/really/nested/file-3.txt rename to imgbom/presenter/json/imgs/test-fixtures/image-simple/target/really/nested/file-3.txt diff --git a/imgbom/presenter/json/test-fixtures/snapshot/TestJsonPresenter.golden b/imgbom/presenter/json/imgs/test-fixtures/snapshot/TestJsonPresenter.golden similarity index 100% rename from imgbom/presenter/json/test-fixtures/snapshot/TestJsonPresenter.golden rename to imgbom/presenter/json/imgs/test-fixtures/snapshot/TestJsonPresenter.golden diff --git a/imgbom/presenter/json/test-fixtures/snapshot/anchore-fixture-image-simple.golden b/imgbom/presenter/json/imgs/test-fixtures/snapshot/anchore-fixture-image-simple.golden similarity index 100% rename from imgbom/presenter/json/test-fixtures/snapshot/anchore-fixture-image-simple.golden rename to imgbom/presenter/json/imgs/test-fixtures/snapshot/anchore-fixture-image-simple.golden From 293b7b6ff960f40a2dfa7975d6c894b02745642f Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Wed, 1 Jul 2020 11:38:39 -0400 Subject: [PATCH 14/19] tests: update integration tests to use new cataloger Signed-off-by: Alfredo Deza --- integration/fixture_image_language_pkgs_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/fixture_image_language_pkgs_test.go b/integration/fixture_image_language_pkgs_test.go index ed04e25c0..8d18e143b 100644 --- a/integration/fixture_image_language_pkgs_test.go +++ b/integration/fixture_image_language_pkgs_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/anchore/go-testutils" - "github.com/anchore/imgbom/imgbom" "github.com/anchore/imgbom/imgbom/cataloger" "github.com/anchore/imgbom/imgbom/pkg" "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") defer cleanup() - catalog, err := imgbom.CatalogImage(img, scope.AllLayersScope) + s, err := scope.NewImageScope(img, scope.AllLayersScope) + catalog, err := cataloger.Catalog(s) if err != nil { t.Fatalf("failed to catalog image: %+v", err) } From f49ca8107ae611c1888b00976dff529f73edb654 Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Wed, 1 Jul 2020 17:17:12 -0400 Subject: [PATCH 15/19] tests: add verification for dirs and imgs Signed-off-by: Alfredo Deza --- imgbom/presenter/json/dirs/presenter_test.go | 53 +++++++++++++++++++ .../snapshot/TestJsonPresenter.golden | 1 + 2 files changed, 54 insertions(+) create mode 100644 imgbom/presenter/json/dirs/presenter_test.go create mode 100644 imgbom/presenter/json/dirs/test-fixtures/snapshot/TestJsonPresenter.golden diff --git a/imgbom/presenter/json/dirs/presenter_test.go b/imgbom/presenter/json/dirs/presenter_test.go new file mode 100644 index 000000000..fbb22564d --- /dev/null +++ b/imgbom/presenter/json/dirs/presenter_test.go @@ -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)) + } + +} diff --git a/imgbom/presenter/json/dirs/test-fixtures/snapshot/TestJsonPresenter.golden b/imgbom/presenter/json/dirs/test-fixtures/snapshot/TestJsonPresenter.golden new file mode 100644 index 000000000..ab2fd8317 --- /dev/null +++ b/imgbom/presenter/json/dirs/test-fixtures/snapshot/TestJsonPresenter.golden @@ -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"} \ No newline at end of file From c35108b00d873e0b6192f0128f5bc9700a80071c Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Thu, 2 Jul 2020 08:16:01 -0400 Subject: [PATCH 16/19] tests: reorganize text img files to match new layout Signed-off-by: Alfredo Deza --- .../text/{ => imgs}/presenter_test.go | 0 .../test-fixtures/image-simple/Dockerfile | 0 .../test-fixtures/image-simple/file-1.txt | 0 .../test-fixtures/image-simple/file-2.txt | 0 .../snapshot/TestTextPresenter.golden | 0 imgbom/presenter/text/presenter.go | 62 ------------------- 6 files changed, 62 deletions(-) rename imgbom/presenter/text/{ => imgs}/presenter_test.go (100%) rename imgbom/presenter/text/{ => imgs}/test-fixtures/image-simple/Dockerfile (100%) rename imgbom/presenter/text/{ => imgs}/test-fixtures/image-simple/file-1.txt (100%) rename imgbom/presenter/text/{ => imgs}/test-fixtures/image-simple/file-2.txt (100%) rename imgbom/presenter/text/{ => imgs}/test-fixtures/snapshot/TestTextPresenter.golden (100%) delete mode 100644 imgbom/presenter/text/presenter.go diff --git a/imgbom/presenter/text/presenter_test.go b/imgbom/presenter/text/imgs/presenter_test.go similarity index 100% rename from imgbom/presenter/text/presenter_test.go rename to imgbom/presenter/text/imgs/presenter_test.go diff --git a/imgbom/presenter/text/test-fixtures/image-simple/Dockerfile b/imgbom/presenter/text/imgs/test-fixtures/image-simple/Dockerfile similarity index 100% rename from imgbom/presenter/text/test-fixtures/image-simple/Dockerfile rename to imgbom/presenter/text/imgs/test-fixtures/image-simple/Dockerfile diff --git a/imgbom/presenter/text/test-fixtures/image-simple/file-1.txt b/imgbom/presenter/text/imgs/test-fixtures/image-simple/file-1.txt similarity index 100% rename from imgbom/presenter/text/test-fixtures/image-simple/file-1.txt rename to imgbom/presenter/text/imgs/test-fixtures/image-simple/file-1.txt diff --git a/imgbom/presenter/text/test-fixtures/image-simple/file-2.txt b/imgbom/presenter/text/imgs/test-fixtures/image-simple/file-2.txt similarity index 100% rename from imgbom/presenter/text/test-fixtures/image-simple/file-2.txt rename to imgbom/presenter/text/imgs/test-fixtures/image-simple/file-2.txt diff --git a/imgbom/presenter/text/test-fixtures/snapshot/TestTextPresenter.golden b/imgbom/presenter/text/imgs/test-fixtures/snapshot/TestTextPresenter.golden similarity index 100% rename from imgbom/presenter/text/test-fixtures/snapshot/TestTextPresenter.golden rename to imgbom/presenter/text/imgs/test-fixtures/snapshot/TestTextPresenter.golden diff --git a/imgbom/presenter/text/presenter.go b/imgbom/presenter/text/presenter.go deleted file mode 100644 index ef7531105..000000000 --- a/imgbom/presenter/text/presenter.go +++ /dev/null @@ -1,62 +0,0 @@ -package text - -import ( - "fmt" - "io" - "text/tabwriter" - - "github.com/anchore/imgbom/imgbom/pkg" - stereoscopeImg "github.com/anchore/stereoscope/pkg/image" -) - -// Presenter holds the Present method to produce output -type Presenter struct { - img *stereoscopeImg.Image - catalog *pkg.Catalog -} - -// NewPresenter is a constructor for a Presenter -func NewPresenter(img *stereoscopeImg.Image, catalog *pkg.Catalog) *Presenter { - return &Presenter{ - img: img, - catalog: catalog, - } -} - -// Present is a method that is in charge of writing to an output buffer -func (pres *Presenter) Present(output io.Writer) error { - tags := make([]string, len(pres.img.Metadata.Tags)) - for idx, tag := range pres.img.Metadata.Tags { - tags[idx] = tag.String() - } - - // init the tabular writer - w := new(tabwriter.Writer) - w.Init(output, 0, 8, 0, '\t', tabwriter.AlignRight) - - fmt.Fprintln(w, "[Image]") - - for idx, l := range pres.img.Layers { - fmt.Fprintln(w, " Layer:\t", idx) - fmt.Fprintln(w, " Digest:\t", l.Metadata.Digest) - fmt.Fprintln(w, " Size:\t", l.Metadata.Size) - fmt.Fprintln(w, " MediaType:\t", l.Metadata.MediaType) - fmt.Fprintln(w) - w.Flush() - } - - // populate artifacts... - 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 -} From 3eedb86a1f4784e29518a00a2323b96ee3976df6 Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Thu, 2 Jul 2020 13:03:45 -0400 Subject: [PATCH 17/19] tests for protocol.go Signed-off-by: Alfredo Deza --- imgbom/protocol_test.go | 43 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 imgbom/protocol_test.go diff --git a/imgbom/protocol_test.go b/imgbom/protocol_test.go new file mode 100644 index 000000000..c76f07db8 --- /dev/null +++ b/imgbom/protocol_test.go @@ -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) + } + + }) + } +} From 3548c7b132dc8bb998a02af77918dcd0230f1ae9 Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Thu, 2 Jul 2020 13:04:08 -0400 Subject: [PATCH 18/19] tests for scope.go Signed-off-by: Alfredo Deza --- imgbom/scope/scope_test.go | 147 ++++++++++++++++++ .../scope/test-fixtures/path-detected/.vimrc | 1 + .../scope/test-fixtures/path-detected/empty | 0 3 files changed, 148 insertions(+) create mode 100644 imgbom/scope/scope_test.go create mode 100644 imgbom/scope/test-fixtures/path-detected/.vimrc create mode 100644 imgbom/scope/test-fixtures/path-detected/empty diff --git a/imgbom/scope/scope_test.go b/imgbom/scope/scope_test.go new file mode 100644 index 000000000..e663c08af --- /dev/null +++ b/imgbom/scope/scope_test.go @@ -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) + } + + }) + } +} diff --git a/imgbom/scope/test-fixtures/path-detected/.vimrc b/imgbom/scope/test-fixtures/path-detected/.vimrc new file mode 100644 index 000000000..93b07e21b --- /dev/null +++ b/imgbom/scope/test-fixtures/path-detected/.vimrc @@ -0,0 +1 @@ +" A .vimrc file diff --git a/imgbom/scope/test-fixtures/path-detected/empty b/imgbom/scope/test-fixtures/path-detected/empty new file mode 100644 index 000000000..e69de29bb From 9baae90c0b04f1a9872344050f44a762c1536d61 Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Thu, 2 Jul 2020 15:39:26 -0400 Subject: [PATCH 19/19] integration: add tests for dir-based presenters Signed-off-by: Alfredo Deza --- integration/dir_presenters_test.go | 77 ++++++ .../snapshot/TestDirJsonPresenter.golden | 1 + .../snapshot/TestDirTextPresenter.golden | 256 ++++++++++++++++++ 3 files changed, 334 insertions(+) create mode 100644 integration/dir_presenters_test.go create mode 100644 integration/test-fixtures/snapshot/TestDirJsonPresenter.golden create mode 100644 integration/test-fixtures/snapshot/TestDirTextPresenter.golden diff --git a/integration/dir_presenters_test.go b/integration/dir_presenters_test.go new file mode 100644 index 000000000..b9233c8cd --- /dev/null +++ b/integration/dir_presenters_test.go @@ -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)) + } + +} diff --git a/integration/test-fixtures/snapshot/TestDirJsonPresenter.golden b/integration/test-fixtures/snapshot/TestDirJsonPresenter.golden new file mode 100644 index 000000000..9406dbebc --- /dev/null +++ b/integration/test-fixtures/snapshot/TestDirJsonPresenter.golden @@ -0,0 +1 @@ +{"artifacts":[{"name":"actionmailer","version":"4.1.1","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"actionpack","version":"4.1.1","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"actionview","version":"4.1.1","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"activemodel","version":"4.1.1","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"activerecord","version":"4.1.1","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"activesupport","version":"4.1.1","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"arel","version":"5.0.1.20140414130214","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"bootstrap-sass","version":"3.1.1.1","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"builder","version":"3.2.2","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"coffee-rails","version":"4.0.1","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"coffee-script","version":"2.2.0","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"coffee-script-source","version":"1.7.0","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"erubis","version":"2.7.0","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"execjs","version":"2.0.2","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"hike","version":"1.2.3","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"i18n","version":"0.6.9","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"jbuilder","version":"2.0.7","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"jquery-rails","version":"3.1.0","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"json","version":"1.8.1","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"kgio","version":"2.9.2","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"libv8","version":"3.16.14.3","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"mail","version":"2.5.4","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"mime-types","version":"1.25.1","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"minitest","version":"5.3.4","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"multi_json","version":"1.10.1","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"mysql2","version":"0.3.16","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"polyglot","version":"0.3.4","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"rack","version":"1.5.2","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"rack-test","version":"0.6.2","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"rails","version":"4.1.1","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"railties","version":"4.1.1","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"raindrops","version":"0.13.0","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"rake","version":"10.3.2","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"rdoc","version":"4.1.1","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"ref","version":"1.0.5","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"sass","version":"3.2.19","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"sass-rails","version":"4.0.3","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"sdoc","version":"0.4.0","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"spring","version":"1.1.3","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"sprockets","version":"2.11.0","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"sprockets-rails","version":"2.1.3","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"sqlite3","version":"1.3.9","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"therubyracer","version":"0.12.1","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"thor","version":"0.19.1","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"thread_safe","version":"0.3.3","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"tilt","version":"1.4.1","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"treetop","version":"1.4.15","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"turbolinks","version":"2.2.2","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"tzinfo","version":"1.2.0","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"uglifier","version":"2.5.0","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null},{"name":"unicorn","version":"4.8.3","type":"bundle","cataloger":"","sources":[{"foundBy":"bundler-cataloger","effects":[]}],"metadata":null}],"Source":"test-fixtures"} \ No newline at end of file diff --git a/integration/test-fixtures/snapshot/TestDirTextPresenter.golden b/integration/test-fixtures/snapshot/TestDirTextPresenter.golden new file mode 100644 index 000000000..de5364a32 --- /dev/null +++ b/integration/test-fixtures/snapshot/TestDirTextPresenter.golden @@ -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 +