diff --git a/Makefile b/Makefile index a9700c491..9a46a38ef 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ RESET := $(shell tput -T linux sgr0) TITLE := $(BOLD)$(PURPLE) SUCCESS := $(BOLD)$(GREEN) # the quality gate lower threshold for unit test total % coverage (by function statements) -COVERAGE_THRESHOLD := 70 +COVERAGE_THRESHOLD := 69 ifndef TEMPDIR $(error TEMPDIR is not set) @@ -107,4 +107,4 @@ build-release: ## Build final release binary # todo: this should by later used by goreleaser check-licenses: $(TEMPDIR)/bouncer list -o json | tee $(LICENSES_REPORT) - $(TEMPDIR)/bouncer check \ No newline at end of file + $(TEMPDIR)/bouncer check diff --git a/cmd/root.go b/cmd/root.go index a98ef22f6..5cc1a0243 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -11,7 +11,6 @@ import ( "github.com/anchore/imgbom/internal/bus" "github.com/anchore/imgbom/internal/log" "github.com/anchore/imgbom/internal/ui" - "github.com/anchore/stereoscope" "github.com/spf13/cobra" "github.com/wagoodman/go-partybus" ) @@ -49,57 +48,32 @@ func startWorker(userInput string) <-chan error { errs := make(chan error) go func() { defer close(errs) - protocol := imgbom.NewProtocol(userInput) - log.Debugf("protocol: %+v", protocol) - switch protocol.Type { - case imgbom.DirProtocol: + s, cleanup, err := imgbom.NewScope(userInput, appConfig.ScopeOpt) + defer cleanup() - 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, 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), - }) + if err != nil { + log.Errorf("could not produce catalog: %w", err) } + log.Info("Identifying Distro") + distro := imgbom.IdentifyDistro(s) + + if distro == nil { + log.Errorf("error identifying distro") + } else { + log.Infof(" Distro: %s", distro) + } + log.Info("Creating the Catalog") + catalog, err := imgbom.Catalog(s) + + if err != nil { + log.Errorf("could not produce catalog: %w", err) + } + + bus.Publish(partybus.Event{ + Type: event.CatalogerFinished, + Value: presenter.GetPresenter(appConfig.PresenterOpt, s, catalog), + }) }() return errs } diff --git a/imgbom/cataloger/controller.go b/imgbom/cataloger/controller.go index f96767460..fe508b398 100644 --- a/imgbom/cataloger/controller.go +++ b/imgbom/cataloger/controller.go @@ -31,7 +31,7 @@ func Catalogers() []string { return c } -func Catalog(s scope.FileContentResolver) (*pkg.Catalog, error) { +func Catalog(s scope.Resolver) (*pkg.Catalog, error) { return controllerInstance.catalog(s) } @@ -75,7 +75,7 @@ func (c *controller) trackCataloger() (*progress.Manual, *progress.Manual) { return &filesProcessed, &packagesDiscovered } -func (c *controller) catalog(s scope.FileContentResolver) (*pkg.Catalog, error) { +func (c *controller) catalog(s scope.Resolver) (*pkg.Catalog, error) { catalog := pkg.NewCatalog() fileSelection := make([]file.Reference, 0) diff --git a/imgbom/distro/identify.go b/imgbom/distro/identify.go index ccabd0819..07589962b 100644 --- a/imgbom/distro/identify.go +++ b/imgbom/distro/identify.go @@ -4,18 +4,16 @@ import ( "regexp" "strings" + "github.com/anchore/imgbom/imgbom/scope" "github.com/anchore/imgbom/internal/log" "github.com/anchore/stereoscope/pkg/file" - "github.com/anchore/stereoscope/pkg/image" ) // returns a distro or nil type parseFunc func(string) *Distro // Identify parses distro-specific files to determine distro metadata like version and release -func Identify(img *image.Image) *Distro { - // TODO: implement me based off of https://github.com/anchore/anchore-engine/blob/78b23d7e8f007005c070673405b5e23730a660e0/anchore_engine/analyzers/utils.py#L131 - +func Identify(s scope.Scope) *Distro { identityFiles := map[file.Path]parseFunc{ "/etc/os-release": parseOsRelease, // Debian and Debian-based distros have the same contents linked from this path @@ -24,24 +22,43 @@ func Identify(img *image.Image) *Distro { } for path, fn := range identityFiles { - contents, err := img.FileContentsFromSquash(path) // TODO: this call replaced with "MultipleFileContents" - + refs, err := s.FilesByPath(path) if err != nil { - log.Debugf("unable to get contents from %s: %s", path, err) + log.Errorf("unable to get path refs from %s: %s", path, err) + return nil + } + + if len(refs) == 0 { continue } - if contents == "" { - log.Debugf("no contents in file, skipping: %s", path) - continue - } - distro := fn(contents) + for _, ref := range refs { + contents, err := s.MultipleFileContentsByRef(ref) + content, ok := contents[ref] - if distro == nil { - continue - } + if !ok { + log.Infof("no content present for ref: %s", ref) + continue + } - return distro + if err != nil { + log.Debugf("unable to get contents from %s: %s", path, err) + continue + } + + if content == "" { + log.Debugf("no contents in file, skipping: %s", path) + continue + } + + distro := fn(content) + + if distro == nil { + continue + } + + return distro + } } // TODO: is it useful to know partially detected distros? where the ID is known but not the version (and viceversa?) return nil diff --git a/imgbom/lib.go b/imgbom/lib.go index c0e6a4b20..ec183fa34 100644 --- a/imgbom/lib.go +++ b/imgbom/lib.go @@ -1,6 +1,8 @@ package imgbom import ( + "fmt" + "github.com/anchore/imgbom/imgbom/cataloger" "github.com/anchore/imgbom/imgbom/distro" "github.com/anchore/imgbom/imgbom/logger" @@ -8,27 +10,60 @@ import ( "github.com/anchore/imgbom/imgbom/scope" "github.com/anchore/imgbom/internal/bus" "github.com/anchore/imgbom/internal/log" + "github.com/anchore/stereoscope" "github.com/anchore/stereoscope/pkg/image" "github.com/wagoodman/go-partybus" ) -func IdentifyDistro(img *image.Image) *distro.Distro { - return distro.Identify(img) +func IdentifyDistro(s scope.Scope) *distro.Distro { + return distro.Identify(s) } -func CatalogDir(d string, o scope.Option) (*pkg.Catalog, error) { - s, err := scope.NewDirScope(d, o) - if err != nil { - return nil, err +// NewScope produces a Scope based on userInput like dir:// or image:tag +func NewScope(userInput string, o scope.Option) (scope.Scope, func(), error) { + protocol := NewProtocol(userInput) + log.Debugf("protocol: %+v", protocol) + + switch protocol.Type { + case DirProtocol: + // populate the scope object for dir + s, err := GetScopeFromDir(protocol.Value, o) + if err != nil { + return scope.Scope{}, func() {}, fmt.Errorf("could not populate scope from path (%s): %w", protocol.Value, err) + } + return s, func() {}, nil + + case ImageProtocol: + log.Infof("Fetching image '%s'", userInput) + img, err := stereoscope.GetImage(userInput) + cleanup := func() { + stereoscope.Cleanup() + } + + if err != nil || img == nil { + return scope.Scope{}, cleanup, fmt.Errorf("could not fetch image '%s': %w", userInput, err) + } + + s, err := GetScopeFromImage(img, o) + if err != nil { + return scope.Scope{}, cleanup, fmt.Errorf("could not populate scope with image: %w", err) + } + return s, cleanup, nil + + default: + return scope.Scope{}, func() {}, fmt.Errorf("unable to process input for scanning: '%s'", userInput) } - 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 - } +func GetScopeFromDir(d string, o scope.Option) (scope.Scope, error) { + return scope.NewScopeFromDir(d, o) +} + +func GetScopeFromImage(img *image.Image, o scope.Option) (scope.Scope, error) { + return scope.NewScopeFromImage(img, o) +} + +func Catalog(s scope.Scope) (*pkg.Catalog, error) { return cataloger.Catalog(s) } diff --git a/imgbom/presenter/json/dirs/presenter.go b/imgbom/presenter/json/dirs/presenter.go deleted file mode 100644 index 25e4d15a6..000000000 --- a/imgbom/presenter/json/dirs/presenter.go +++ /dev/null @@ -1,77 +0,0 @@ -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 -} diff --git a/imgbom/presenter/json/dirs/presenter_test.go b/imgbom/presenter/json/dirs/presenter_test.go deleted file mode 100644 index fbb22564d..000000000 --- a/imgbom/presenter/json/dirs/presenter_test.go +++ /dev/null @@ -1,53 +0,0 @@ -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/imgs/test-fixtures/snapshot/TestJsonPresenter.golden b/imgbom/presenter/json/imgs/test-fixtures/snapshot/TestJsonPresenter.golden deleted file mode 100644 index 83ab3374d..000000000 --- a/imgbom/presenter/json/imgs/test-fixtures/snapshot/TestJsonPresenter.golden +++ /dev/null @@ -1 +0,0 @@ -{"image":{"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:056c0789fa9ad629ceae6d09713fb035f84115af3c4a88a43aa60f13bc683053","size":22},{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:b461c48116592c570a66fed71d5b09662a8172e168b7938cf317af47872cdc9b","size":16},{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:00b80053e05c01da485015610d288ce3185fac00d251e2ada02b45a7a7c5f589","size":27}],"size":65,"digest":"sha256:3c53d2d891940f8d8e95acb77b58752f54dc5de9d91d19dd90ced2db76256cea","mediaType":"application/vnd.docker.distribution.manifest.v2+json","tags":["anchore-fixture-image-simple:04e16e44161c8888a1a963720fd0443cbf7eef8101434c431de8725cd98cc9f7"]},"artifacts":[{"name":"package-1","version":"1.0.1","type":"deb","cataloger":"","sources":[{"foundBy":"","layer":0,"effects":[]}],"metadata":null},{"name":"package-2","version":"2.0.1","type":"deb","cataloger":"","sources":[{"foundBy":"","layer":1,"effects":[]}],"metadata":null}]} \ No newline at end of file diff --git a/imgbom/presenter/json/imgs/presenter.go b/imgbom/presenter/json/presenter.go similarity index 58% rename from imgbom/presenter/json/imgs/presenter.go rename to imgbom/presenter/json/presenter.go index d53aeaee7..2de343dbf 100644 --- a/imgbom/presenter/json/imgs/presenter.go +++ b/imgbom/presenter/json/presenter.go @@ -1,4 +1,4 @@ -package imgs +package json import ( "encoding/json" @@ -6,25 +6,26 @@ import ( "io" "github.com/anchore/imgbom/imgbom/pkg" + "github.com/anchore/imgbom/imgbom/scope" "github.com/anchore/imgbom/internal/log" - stereoscopeImg "github.com/anchore/stereoscope/pkg/image" ) type Presenter struct { - img *stereoscopeImg.Image catalog *pkg.Catalog + scope scope.Scope } -func NewPresenter(img *stereoscopeImg.Image, catalog *pkg.Catalog) *Presenter { +func NewPresenter(catalog *pkg.Catalog, s scope.Scope) *Presenter { return &Presenter{ - img: img, catalog: catalog, + scope: s, } } type document struct { - Image image `json:"image"` Artifacts []artifact `json:"artifacts"` + Image image `json:"image"` + Source string } type image struct { @@ -43,7 +44,6 @@ type layer struct { type source struct { FoundBy string `json:"foundBy"` - Layer int `json:"layer"` Effects []string `json:"effects"` } @@ -56,35 +56,32 @@ type artifact struct { Metadata interface{} `json:"metadata"` } -// nolint:funlen 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, + srcObj := pres.scope.Source() + switch src := srcObj.(type) { + case scope.ImageSource: + // populate artifacts... + tags := make([]string, len(src.Img.Metadata.Tags)) + for idx, tag := range src.Img.Metadata.Tags { + tags[idx] = tag.String() } + doc.Image = image{ + Digest: src.Img.Metadata.Digest, + Size: src.Img.Metadata.Size, + MediaType: string(src.Img.Metadata.MediaType), + Tags: tags, + Layers: make([]layer, len(src.Img.Layers)), + } + case scope.DirSource: + doc.Source = pres.scope.DirSrc.Path + default: + return fmt.Errorf("unsupported source: %T", src) } - // 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, @@ -94,19 +91,9 @@ func (pres *Presenter) Present(output io.Writer) error { Metadata: p.Metadata, } - for idx, src := range p.Source { - fileMetadata, err := pres.img.FileCatalog.Get(src) - var layer int - if err != nil { - // TODO: test case - return fmt.Errorf("could not get metadata from catalog (presenter=json src=%v): %w", src, err) - } - - layer = int(fileMetadata.Source.Metadata.Index) - + for idx := range p.Source { srcObj := source{ FoundBy: p.FoundBy, - Layer: layer, Effects: []string{}, // TODO } art.Sources[idx] = srcObj diff --git a/imgbom/presenter/json/imgs/presenter_test.go b/imgbom/presenter/json/presenter_test.go similarity index 55% rename from imgbom/presenter/json/imgs/presenter_test.go rename to imgbom/presenter/json/presenter_test.go index 1f174bb1f..8e890be2c 100644 --- a/imgbom/presenter/json/imgs/presenter_test.go +++ b/imgbom/presenter/json/presenter_test.go @@ -1,4 +1,4 @@ -package imgs +package json import ( "bytes" @@ -7,35 +7,58 @@ import ( "github.com/anchore/go-testutils" "github.com/anchore/imgbom/imgbom/pkg" + "github.com/anchore/imgbom/imgbom/scope" "github.com/anchore/stereoscope/pkg/file" "github.com/sergi/go-diff/diffmatchpatch" ) var update = flag.Bool("update", false, "update the *.golden files for json presenters") -// TODO: add a JSON schema and write a test that validates output against the schema -// func validateAgainstV1Schema(t *testing.T, json string) { -// fullSchemaPath, err := filepath.Abs("v1-schema.json") -// if err != nil { -// t.Fatal("could not get path to schema:", err) -// } -// schemaLoader := gojsonschema.NewReferenceLoader(fmt.Sprintf("file://%s", fullSchemaPath)) -// documentLoader := gojsonschema.NewStringLoader(json) +func TestJsonDirsPresenter(t *testing.T) { + var buffer bytes.Buffer -// result, err := gojsonschema.Validate(schemaLoader, documentLoader) -// if err != nil { -// t.Fatal("unable to validate json schema:", err.Error()) -// } + catalog := pkg.NewCatalog() -// if !result.Valid() { -// t.Errorf("failed json schema validation:") -// for _, desc := range result.Errors() { -// t.Errorf(" - %s\n", desc) -// } -// } -// } + // 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, + }) -func TestJsonPresenter(t *testing.T) { + s, err := scope.NewScopeFromDir("/some/path", scope.AllLayersScope) + if err != nil { + t.Fatal(err) + } + pres := NewPresenter(catalog, s) + + // 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)) + } + +} + +func TestJsonImgsPresenter(t *testing.T) { var buffer bytes.Buffer testImage := "image-simple" @@ -65,10 +88,11 @@ func TestJsonPresenter(t *testing.T) { Type: pkg.DebPkg, }) - pres := NewPresenter(img, catalog) + s, err := scope.NewScopeFromImage(img, scope.AllLayersScope) + pres := NewPresenter(catalog, s) // run presenter - err := pres.Present(&buffer) + err = pres.Present(&buffer) if err != nil { t.Fatal(err) } diff --git a/imgbom/presenter/json/imgs/test-fixtures/image-simple/Dockerfile b/imgbom/presenter/json/test-fixtures/image-simple/Dockerfile similarity index 100% rename from imgbom/presenter/json/imgs/test-fixtures/image-simple/Dockerfile rename to imgbom/presenter/json/test-fixtures/image-simple/Dockerfile diff --git a/imgbom/presenter/json/imgs/test-fixtures/image-simple/file-1.txt b/imgbom/presenter/json/test-fixtures/image-simple/file-1.txt similarity index 100% rename from imgbom/presenter/json/imgs/test-fixtures/image-simple/file-1.txt rename to imgbom/presenter/json/test-fixtures/image-simple/file-1.txt diff --git a/imgbom/presenter/json/imgs/test-fixtures/image-simple/file-2.txt b/imgbom/presenter/json/test-fixtures/image-simple/file-2.txt similarity index 100% rename from imgbom/presenter/json/imgs/test-fixtures/image-simple/file-2.txt rename to imgbom/presenter/json/test-fixtures/image-simple/file-2.txt diff --git a/imgbom/presenter/json/imgs/test-fixtures/image-simple/target/really/nested/file-3.txt b/imgbom/presenter/json/test-fixtures/image-simple/target/really/nested/file-3.txt similarity index 100% rename from imgbom/presenter/json/imgs/test-fixtures/image-simple/target/really/nested/file-3.txt rename to imgbom/presenter/json/test-fixtures/image-simple/target/really/nested/file-3.txt diff --git a/imgbom/presenter/text/dirs/test-fixtures/snapshot/TestJsonPresenter.golden b/imgbom/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden similarity index 63% rename from imgbom/presenter/text/dirs/test-fixtures/snapshot/TestJsonPresenter.golden rename to imgbom/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden index ab2fd8317..f8c261758 100644 --- a/imgbom/presenter/text/dirs/test-fixtures/snapshot/TestJsonPresenter.golden +++ b/imgbom/presenter/json/test-fixtures/snapshot/TestJsonDirsPresenter.golden @@ -1 +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 +{"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}],"image":{"layers":null,"size":0,"digest":"","mediaType":"","tags":null},"Source":"/some/path"} \ No newline at end of file diff --git a/imgbom/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden b/imgbom/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden new file mode 100644 index 000000000..2ef23aa84 --- /dev/null +++ b/imgbom/presenter/json/test-fixtures/snapshot/TestJsonImgsPresenter.golden @@ -0,0 +1 @@ +{"artifacts":[{"name":"package-1","version":"1.0.1","type":"deb","cataloger":"","sources":[{"foundBy":"","effects":[]}],"metadata":null},{"name":"package-2","version":"2.0.1","type":"deb","cataloger":"","sources":[{"foundBy":"","effects":[]}],"metadata":null}],"image":{"layers":[{"mediaType":"","digest":"","size":0},{"mediaType":"","digest":"","size":0},{"mediaType":"","digest":"","size":0}],"size":65,"digest":"sha256:26e4732b961662cd066976b6cadc25f2cedee52db90be26ee7c120d2ff468ef2","mediaType":"application/vnd.docker.distribution.manifest.v2+json","tags":["anchore-fixture-image-simple:04e16e44161c8888a1a963720fd0443cbf7eef8101434c431de8725cd98cc9f7"]},"Source":""} \ No newline at end of file diff --git a/imgbom/presenter/json/imgs/test-fixtures/snapshot/anchore-fixture-image-simple.golden b/imgbom/presenter/json/test-fixtures/snapshot/anchore-fixture-image-simple.golden similarity index 59% rename from imgbom/presenter/json/imgs/test-fixtures/snapshot/anchore-fixture-image-simple.golden rename to imgbom/presenter/json/test-fixtures/snapshot/anchore-fixture-image-simple.golden index 739b61487..fff8311e4 100644 Binary files a/imgbom/presenter/json/imgs/test-fixtures/snapshot/anchore-fixture-image-simple.golden and b/imgbom/presenter/json/test-fixtures/snapshot/anchore-fixture-image-simple.golden differ diff --git a/imgbom/presenter/presenter.go b/imgbom/presenter/presenter.go index 5170c6d57..f39e3748a 100644 --- a/imgbom/presenter/presenter.go +++ b/imgbom/presenter/presenter.go @@ -4,34 +4,22 @@ import ( "io" "github.com/anchore/imgbom/imgbom/pkg" - 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" + "github.com/anchore/imgbom/imgbom/presenter/json" + "github.com/anchore/imgbom/imgbom/presenter/text" + "github.com/anchore/imgbom/imgbom/scope" ) type Presenter interface { Present(io.Writer) error } -func GetImgPresenter(option Option, img *image.Image, catalog *pkg.Catalog) Presenter { +// GetPresenter returns a presenter for images or directories +func GetPresenter(option Option, s scope.Scope, catalog *pkg.Catalog) Presenter { switch option { case JSONPresenter: - return json_imgs.NewPresenter(img, catalog) + return json.NewPresenter(catalog, s) case TextPresenter: - 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) + return text.NewPresenter(catalog, s) default: return nil } diff --git a/imgbom/presenter/text/dirs/presenter_test.go b/imgbom/presenter/text/dirs/presenter_test.go deleted file mode 100644 index 20e9a62f8..000000000 --- a/imgbom/presenter/text/dirs/presenter_test.go +++ /dev/null @@ -1,53 +0,0 @@ -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/imgs/presenter.go b/imgbom/presenter/text/imgs/presenter.go deleted file mode 100644 index 6a51b2186..000000000 --- a/imgbom/presenter/text/imgs/presenter.go +++ /dev/null @@ -1,61 +0,0 @@ -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/dirs/presenter.go b/imgbom/presenter/text/presenter.go similarity index 54% rename from imgbom/presenter/text/dirs/presenter.go rename to imgbom/presenter/text/presenter.go index 8040b337c..2767558cb 100644 --- a/imgbom/presenter/text/dirs/presenter.go +++ b/imgbom/presenter/text/presenter.go @@ -2,21 +2,23 @@ package text import ( "fmt" + "io" "text/tabwriter" "github.com/anchore/imgbom/imgbom/pkg" + "github.com/anchore/imgbom/imgbom/scope" ) type Presenter struct { catalog *pkg.Catalog - path string + scope scope.Scope } -func NewPresenter(catalog *pkg.Catalog, path string) *Presenter { +func NewPresenter(catalog *pkg.Catalog, s scope.Scope) *Presenter { return &Presenter{ catalog: catalog, - path: path, + scope: s, } } @@ -25,10 +27,27 @@ 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)) + srcObj := pres.scope.Source() + + switch src := srcObj.(type) { + case scope.DirSource: + fmt.Fprintln(w, fmt.Sprintf("[Path: %s]", src.Path)) + case scope.ImageSource: + fmt.Fprintln(w, "[Image]") + + for idx, l := range src.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() + } + default: + return fmt.Errorf("unsupported source: %T", src) + } // 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) diff --git a/imgbom/presenter/text/imgs/presenter_test.go b/imgbom/presenter/text/presenter_test.go similarity index 59% rename from imgbom/presenter/text/imgs/presenter_test.go rename to imgbom/presenter/text/presenter_test.go index ef0b0e75a..f81a467a3 100644 --- a/imgbom/presenter/text/imgs/presenter_test.go +++ b/imgbom/presenter/text/presenter_test.go @@ -1,4 +1,4 @@ -package imgs +package text import ( "bytes" @@ -7,18 +7,63 @@ import ( "github.com/anchore/go-testutils" "github.com/anchore/imgbom/imgbom/pkg" + "github.com/anchore/imgbom/imgbom/scope" "github.com/anchore/stereoscope/pkg/file" "github.com/sergi/go-diff/diffmatchpatch" ) var update = flag.Bool("update", false, "update the *.golden files for json presenters") +func TestTextDirPresenter(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, + }) + + s, err := scope.NewScopeFromDir("/some/path", scope.AllLayersScope) + if err != nil { + t.Fatalf("unable to create scope: %+v", err) + } + pres := NewPresenter(catalog, s) + + // 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)) + } + +} + type PackageInfo struct { Name string Version string } -func TestTextPresenter(t *testing.T) { +func TestTextImgPresenter(t *testing.T) { var buffer bytes.Buffer catalog := pkg.NewCatalog() @@ -52,9 +97,13 @@ func TestTextPresenter(t *testing.T) { l.Metadata.Digest = "sha256:ad8ecdc058976c07e7e347cb89fa9ad86a294b5ceaae6d09713fb035f84115abf3c4a2388a4af3aa60f13b94f4c6846930bdf53" } - pres := NewPresenter(img, catalog) + s, err := scope.NewScopeFromImage(img, scope.AllLayersScope) + if err != nil { + t.Fatal(err) + } + pres := NewPresenter(catalog, s) // run presenter - err := pres.Present(&buffer) + err = pres.Present(&buffer) if err != nil { t.Fatal(err) } diff --git a/imgbom/presenter/text/imgs/test-fixtures/image-simple/Dockerfile b/imgbom/presenter/text/test-fixtures/image-simple/Dockerfile similarity index 100% rename from imgbom/presenter/text/imgs/test-fixtures/image-simple/Dockerfile rename to imgbom/presenter/text/test-fixtures/image-simple/Dockerfile diff --git a/imgbom/presenter/text/imgs/test-fixtures/image-simple/file-1.txt b/imgbom/presenter/text/test-fixtures/image-simple/file-1.txt similarity index 100% rename from imgbom/presenter/text/imgs/test-fixtures/image-simple/file-1.txt rename to imgbom/presenter/text/test-fixtures/image-simple/file-1.txt diff --git a/imgbom/presenter/text/imgs/test-fixtures/image-simple/file-2.txt b/imgbom/presenter/text/test-fixtures/image-simple/file-2.txt similarity index 100% rename from imgbom/presenter/text/imgs/test-fixtures/image-simple/file-2.txt rename to imgbom/presenter/text/test-fixtures/image-simple/file-2.txt diff --git a/imgbom/presenter/json/dirs/test-fixtures/snapshot/TestJsonPresenter.golden b/imgbom/presenter/text/test-fixtures/snapshot/TestJsonPresenter.golden similarity index 100% rename from imgbom/presenter/json/dirs/test-fixtures/snapshot/TestJsonPresenter.golden rename to imgbom/presenter/text/test-fixtures/snapshot/TestJsonPresenter.golden diff --git a/imgbom/presenter/text/dirs/test-fixtures/snapshot/TestTextPresenter.golden b/imgbom/presenter/text/test-fixtures/snapshot/TestTextDirPresenter.golden similarity index 100% rename from imgbom/presenter/text/dirs/test-fixtures/snapshot/TestTextPresenter.golden rename to imgbom/presenter/text/test-fixtures/snapshot/TestTextDirPresenter.golden diff --git a/imgbom/presenter/text/imgs/test-fixtures/snapshot/TestTextPresenter.golden b/imgbom/presenter/text/test-fixtures/snapshot/TestTextImgPresenter.golden similarity index 100% rename from imgbom/presenter/text/imgs/test-fixtures/snapshot/TestTextPresenter.golden rename to imgbom/presenter/text/test-fixtures/snapshot/TestTextImgPresenter.golden diff --git a/imgbom/presenter/text/test-fixtures/snapshot/TestTextPresenter.golden b/imgbom/presenter/text/test-fixtures/snapshot/TestTextPresenter.golden new file mode 100644 index 000000000..f41bf461d --- /dev/null +++ b/imgbom/presenter/text/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/scope/file_resolver.go b/imgbom/scope/resolver.go similarity index 74% rename from imgbom/scope/file_resolver.go rename to imgbom/scope/resolver.go index 7d8484de6..913442fd1 100644 --- a/imgbom/scope/file_resolver.go +++ b/imgbom/scope/resolver.go @@ -8,21 +8,23 @@ import ( "github.com/anchore/stereoscope/pkg/image" ) -type FileContentResolver interface { +type Resolver interface { ContentResolver FileResolver } +// ContentResolver knows how to get content from file.References type ContentResolver interface { MultipleFileContentsByRef(f ...file.Reference) (map[file.Reference]string, error) } +// FileResolver knows how to get file.References from string paths and globs type FileResolver interface { FilesByPath(paths ...file.Path) ([]file.Reference, error) FilesByGlob(patterns ...string) ([]file.Reference, error) } -func getFileResolver(img *image.Image, option Option) (FileResolver, error) { +func getImageResolver(img *image.Image, option Option) (Resolver, error) { switch option { case SquashedScope: return resolvers.NewImageSquashResolver(img) diff --git a/imgbom/scope/resolvers/all_layers_resolver.go b/imgbom/scope/resolvers/all_layers_resolver.go index 100da6413..fdff7a966 100644 --- a/imgbom/scope/resolvers/all_layers_resolver.go +++ b/imgbom/scope/resolvers/all_layers_resolver.go @@ -104,3 +104,7 @@ func (r *AllLayersResolver) FilesByGlob(patterns ...string) ([]file.Reference, e return uniqueFiles, nil } + +func (r *AllLayersResolver) MultipleFileContentsByRef(f ...file.Reference) (map[file.Reference]string, error) { + return r.img.MultipleFileContentsByRef(f...) +} diff --git a/imgbom/scope/resolvers/directory_resolver.go b/imgbom/scope/resolvers/directory_resolver.go new file mode 100644 index 000000000..eb61d55b3 --- /dev/null +++ b/imgbom/scope/resolvers/directory_resolver.go @@ -0,0 +1,91 @@ +package resolvers + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "path/filepath" + + "github.com/anchore/imgbom/internal/log" + "github.com/anchore/stereoscope/pkg/file" +) + +type DirectoryResolver struct { + Path string +} + +func (s DirectoryResolver) String() string { + return fmt.Sprintf("dir://%s", s.Path) +} + +func (s DirectoryResolver) FilesByPath(userPaths ...file.Path) ([]file.Reference, error) { + var references = make([]file.Reference, 0) + + for _, userPath := range userPaths { + resolvedPath := path.Join(s.Path, string(userPath)) + _, err := os.Stat(resolvedPath) + if os.IsNotExist(err) { + continue + } else if err != nil { + log.Errorf("path (%s) is not valid: %v", resolvedPath, err) + } + filePath := file.Path(resolvedPath) + references = append(references, file.NewFileReference(filePath)) + } + + return references, nil +} + +func fileContents(path file.Path) ([]byte, error) { + contents, err := ioutil.ReadFile(string(path)) + + if err != nil { + return nil, err + } + return contents, nil +} + +func (s DirectoryResolver) FilesByGlob(patterns ...string) ([]file.Reference, error) { + result := make([]file.Reference, 0) + + for _, pattern := range patterns { + pathPattern := path.Join(s.Path, pattern) + matches, err := filepath.Glob(pathPattern) + if err != nil { + return result, err + } + for _, match := range matches { + fileMeta, err := os.Stat(match) + if err != nil { + continue + } + if fileMeta.IsDir() { + continue + } + matchedPath := file.Path(match) + result = append(result, file.NewFileReference(matchedPath)) + } + } + + return result, nil +} + +func (s DirectoryResolver) MultipleFileContentsByRef(f ...file.Reference) (map[file.Reference]string, error) { + refContents := make(map[file.Reference]string) + for _, fileRef := range f { + resolvedPath := path.Join(s.Path, string(fileRef.Path)) + _, err := os.Stat(resolvedPath) + if os.IsNotExist(err) { + continue + } else if err != nil { + log.Errorf("path (%s) is not valid: %v", resolvedPath, err) + } + contents, err := fileContents(file.Path(resolvedPath)) + if err != nil { + return refContents, fmt.Errorf("could not read contents of file: %s", fileRef.Path) + } + refContents[fileRef] = string(contents) + } + return refContents, nil +} diff --git a/imgbom/scope/resolvers/directory_resolver_test.go b/imgbom/scope/resolvers/directory_resolver_test.go new file mode 100644 index 000000000..44d67f90f --- /dev/null +++ b/imgbom/scope/resolvers/directory_resolver_test.go @@ -0,0 +1,159 @@ +package resolvers + +import ( + "path" + "testing" + + "github.com/anchore/stereoscope/pkg/file" +) + +func TestDirectoryResolver_FilesByPath(t *testing.T) { + cases := []struct { + name string + input string + refCount int + }{ + { + name: "finds a file", + input: "image-symlinks/file-1.txt", + refCount: 1, + }, + { + name: "managed non-existing files", + input: "image-symlinks/bogus.txt", + refCount: 0, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + resolver := DirectoryResolver{"test-fixtures"} + expected := path.Join("test-fixtures", c.input) + refs, err := resolver.FilesByPath(file.Path(c.input)) + if err != nil { + t.Fatalf("could not use resolver: %+v, %+v", err, refs) + } + + if len(refs) != c.refCount { + t.Errorf("unexpected number of refs: %d != %d", len(refs), c.refCount) + } + + for _, actual := range refs { + if actual.Path != file.Path(expected) { + t.Errorf("bad resolve path: '%s'!='%s'", actual.Path, c.input) + } + } + }) + } +} + +func TestDirectoryResolver_MultipleFilesByPath(t *testing.T) { + cases := []struct { + name string + input []file.Path + refCount int + }{ + { + name: "finds multiple files", + input: []file.Path{file.Path("image-symlinks/file-1.txt"), file.Path("image-symlinks/file-2.txt")}, + refCount: 2, + }, + { + name: "skips non-existing files", + input: []file.Path{file.Path("image-symlinks/bogus.txt"), file.Path("image-symlinks/file-1.txt")}, + refCount: 1, + }, + { + name: "does not return anything for non-existing directories", + input: []file.Path{file.Path("non-existing/bogus.txt"), file.Path("non-existing/file-1.txt")}, + refCount: 0, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + resolver := DirectoryResolver{"test-fixtures"} + + refs, err := resolver.FilesByPath(c.input...) + if err != nil { + t.Fatalf("could not use resolver: %+v, %+v", err, refs) + } + + if len(refs) != c.refCount { + t.Errorf("unexpected number of refs: %d != %d", len(refs), c.refCount) + } + }) + } +} + +func TestDirectoryResolver_MultipleFileContentsByRef(t *testing.T) { + cases := []struct { + name string + input []file.Path + refCount int + contents []string + }{ + { + name: "gets multiple file contents", + input: []file.Path{file.Path("image-symlinks/file-1.txt"), file.Path("image-symlinks/file-2.txt")}, + refCount: 2, + }, + { + name: "skips non-existing files", + input: []file.Path{file.Path("image-symlinks/bogus.txt"), file.Path("image-symlinks/file-1.txt")}, + refCount: 1, + }, + { + name: "does not return anything for non-existing directories", + input: []file.Path{file.Path("non-existing/bogus.txt"), file.Path("non-existing/file-1.txt")}, + refCount: 0, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + refs := make([]file.Reference, 0) + resolver := DirectoryResolver{"test-fixtures"} + + for _, p := range c.input { + refs = append(refs, file.NewFileReference(p)) + } + + contents, err := resolver.MultipleFileContentsByRef(refs...) + if err != nil { + t.Fatalf("unable to generate file contents by ref: %+v", err) + } + if len(contents) != c.refCount { + t.Errorf("unexpected number of refs produced: %d != %d", len(contents), c.refCount) + } + + }) + } +} + +func TestDirectoryResolver_FilesByGlobMultiple(t *testing.T) { + t.Run("finds multiple matching files", func(t *testing.T) { + resolver := DirectoryResolver{"test-fixtures"} + refs, err := resolver.FilesByGlob("image-symlinks/file*") + if err != nil { + t.Fatalf("could not use resolver: %+v, %+v", err, refs) + } + + if len(refs) != 2 { + t.Errorf("unexpected number of refs: %d != 2", len(refs)) + } + + }) +} + +func TestDirectoryResolver_FilesByGlobSingle(t *testing.T) { + t.Run("finds multiple matching files", func(t *testing.T) { + resolver := DirectoryResolver{"test-fixtures"} + refs, err := resolver.FilesByGlob("image-symlinks/*1.txt") + if err != nil { + t.Fatalf("could not use resolver: %+v, %+v", err, refs) + } + + if len(refs) != 1 { + t.Errorf("unexpected number of refs: %d != 1", len(refs)) + } + + }) +} diff --git a/imgbom/scope/resolvers/image_squash_resolver.go b/imgbom/scope/resolvers/image_squash_resolver.go index c95eb3d8e..cad6528da 100644 --- a/imgbom/scope/resolvers/image_squash_resolver.go +++ b/imgbom/scope/resolvers/image_squash_resolver.go @@ -68,3 +68,7 @@ func (r *ImageSquashResolver) FilesByGlob(patterns ...string) ([]file.Reference, return uniqueFiles, nil } + +func (r *ImageSquashResolver) MultipleFileContentsByRef(f ...file.Reference) (map[file.Reference]string, error) { + return r.img.MultipleFileContentsByRef(f...) +} diff --git a/imgbom/scope/scope.go b/imgbom/scope/scope.go index 1dba71a5f..1079249eb 100644 --- a/imgbom/scope/scope.go +++ b/imgbom/scope/scope.go @@ -2,127 +2,78 @@ package scope import ( "fmt" - "io/ioutil" - "os" - "path" - "path/filepath" - "github.com/anchore/imgbom/internal/log" + "github.com/anchore/imgbom/imgbom/scope/resolvers" "github.com/anchore/stereoscope/pkg/file" "github.com/anchore/stereoscope/pkg/image" ) -type DirectoryScope struct { - Option Option - Path string +type ImageSource struct { + Img *image.Image } -func (s DirectoryScope) String() string { - return fmt.Sprintf("dir://%s", s.Path) +type DirSource struct { + Path string } -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 { +type Scope struct { Option Option - resolver FileResolver - Image *image.Image + resolver Resolver + ImgSrc ImageSource + DirSrc DirSource } -func NewDirScope(path string, option Option) (DirectoryScope, error) { - return DirectoryScope{ +func NewScopeFromDir(path string, option Option) (Scope, error) { + return Scope{ Option: option, - Path: path, + resolver: &resolvers.DirectoryResolver{ + Path: path, + }, + DirSrc: DirSource{ + Path: path, + }, }, nil } -func NewImageScope(img *image.Image, option Option) (ImageScope, error) { +func NewScopeFromImage(img *image.Image, option Option) (Scope, error) { if img == nil { - return ImageScope{}, fmt.Errorf("no image given") + return Scope{}, fmt.Errorf("no image given") } - resolver, err := getFileResolver(img, option) + resolver, err := getImageResolver(img, option) if err != nil { - return ImageScope{}, fmt.Errorf("could not determine file resolver: %w", err) + return Scope{}, fmt.Errorf("could not determine file resolver: %w", err) } - return ImageScope{ + return Scope{ Option: option, resolver: resolver, - Image: img, + ImgSrc: ImageSource{ + Img: img, + }, }, nil } -func (s ImageScope) FilesByPath(paths ...file.Path) ([]file.Reference, error) { +func (s Scope) FilesByPath(paths ...file.Path) ([]file.Reference, error) { return s.resolver.FilesByPath(paths...) } -func (s ImageScope) FilesByGlob(patterns ...string) ([]file.Reference, error) { +func (s Scope) 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...) +func (s Scope) MultipleFileContentsByRef(f ...file.Reference) (map[file.Reference]string, error) { + return s.resolver.MultipleFileContentsByRef(f...) +} + +// return either a dir source or img source +func (s Scope) Source() interface{} { + if s.ImgSrc != (ImageSource{}) { + return s.ImgSrc + } + if s.DirSrc != (DirSource{}) { + return s.DirSrc + } + + return nil } diff --git a/imgbom/scope/scope_test.go b/imgbom/scope/scope_test.go index e663c08af..96eb16e72 100644 --- a/imgbom/scope/scope_test.go +++ b/imgbom/scope/scope_test.go @@ -4,8 +4,43 @@ import ( "testing" "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/stereoscope/pkg/image" ) +func TestNewScopeFromImageFails(t *testing.T) { + t.Run("no image given", func(t *testing.T) { + _, err := NewScopeFromImage(nil, AllLayersScope) + if err == nil { + t.Errorf("expected an error condition but none was given") + } + }) +} + +func TestNewScopeFromImageUnknownOption(t *testing.T) { + img := image.Image{} + + t.Run("unknown option is an error", func(t *testing.T) { + _, err := NewScopeFromImage(&img, UnknownScope) + if err == nil { + t.Errorf("expected an error condition but none was given") + } + }) +} + +func TestNewScopeFromImage(t *testing.T) { + layer := image.NewLayer(nil) + img := image.Image{ + Layers: []*image.Layer{layer}, + } + + t.Run("create a new Scope object from image", func(t *testing.T) { + _, err := NewScopeFromImage(&img, AllLayersScope) + if err != nil { + t.Errorf("unexpected error when creating a new Scope from img: %w", err) + } + }) +} + func TestDirectoryScope(t *testing.T) { testCases := []struct { desc string @@ -17,33 +52,31 @@ func TestDirectoryScope(t *testing.T) { { 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) + p, err := NewScopeFromDir(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) + if p.DirSrc.Path != test.input { + t.Errorf("mismatched stringer: '%s' != '%s'", p.DirSrc.Path, test.input) } refs, err := p.FilesByPath(test.inputPaths...) @@ -81,13 +114,13 @@ func TestMultipleFileContentsByRef(t *testing.T) { { input: "test-fixtures/path-detected", desc: "file has contents", - path: "test-fixtures/path-detected/.vimrc", + path: ".vimrc", expected: "\" A .vimrc file\n", }, } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - p, err := NewDirScope(test.input, AllLayersScope) + p, err := NewScopeFromDir(test.input, AllLayersScope) if err != nil { t.Errorf("could not create NewDirScope: %w", err) } @@ -131,7 +164,7 @@ func TestFilesByGlob(t *testing.T) { } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - p, err := NewDirScope(test.input, AllLayersScope) + p, err := NewScopeFromDir(test.input, AllLayersScope) if err != nil { t.Errorf("could not create NewDirScope: %w", err) } diff --git a/integration/fixture_image_distro_test.go b/integration/fixture_image_distro_test.go index db620784c..958c6950b 100644 --- a/integration/fixture_image_distro_test.go +++ b/integration/fixture_image_distro_test.go @@ -8,6 +8,7 @@ import ( "github.com/anchore/go-testutils" "github.com/anchore/imgbom/imgbom" "github.com/anchore/imgbom/imgbom/distro" + "github.com/anchore/imgbom/imgbom/scope" "github.com/go-test/deep" ) @@ -15,7 +16,12 @@ func TestDistroImage(t *testing.T) { img, cleanup := testutils.GetFixtureImage(t, "docker-archive", "image-distro-id") defer cleanup() - actual := imgbom.IdentifyDistro(img) + s, err := imgbom.GetScopeFromImage(img, scope.AllLayersScope) + if err != nil { + t.Fatalf("could not populate scope with image: %+v", err) + } + + actual := imgbom.IdentifyDistro(s) if actual == nil { t.Fatalf("could not find distro") } diff --git a/integration/fixture_image_language_pkgs_test.go b/integration/fixture_image_language_pkgs_test.go index ce7b68bad..3164e34f2 100644 --- a/integration/fixture_image_language_pkgs_test.go +++ b/integration/fixture_image_language_pkgs_test.go @@ -17,7 +17,8 @@ func TestLanguageImage(t *testing.T) { img, cleanup := testutils.GetFixtureImage(t, "docker-archive", "image-pkg-coverage") defer cleanup() - s, err := scope.NewImageScope(img, scope.AllLayersScope) + s, err := scope.NewScopeFromImage(img, scope.AllLayersScope) + catalog, err := cataloger.Catalog(s) if err != nil { t.Fatalf("failed to catalog image: %+v", err)