promote catalog task pattern to all commands (#636)

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
This commit is contained in:
Christopher Angelo Phillips 2021-11-19 13:26:23 -05:00 committed by GitHub
parent d76c868481
commit 4f0099583a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 112 additions and 82 deletions

View File

@ -48,7 +48,13 @@ func initCmdAliasBindings() {
panic(err) panic(err)
} }
if activeCmd == packagesCmd || activeCmd == rootCmd { // enable all cataloger by default if power-user command is run
if activeCmd == powerUserCmd {
config.PowerUserCatalogerEnabledDefault()
}
switch activeCmd {
case packagesCmd, rootCmd:
// note: we need to lazily bind config options since they are shared between both the root command // note: we need to lazily bind config options since they are shared between both the root command
// and the packages command. Otherwise there will be global viper state that is in contention. // and the packages command. Otherwise there will be global viper state that is in contention.
// See for more details: https://github.com/spf13/viper/issues/233 . Additionally, the bindings must occur BEFORE // See for more details: https://github.com/spf13/viper/issues/233 . Additionally, the bindings must occur BEFORE
@ -58,7 +64,7 @@ func initCmdAliasBindings() {
if err = bindPackagesConfigOptions(activeCmd.Flags()); err != nil { if err = bindPackagesConfigOptions(activeCmd.Flags()); err != nil {
panic(err) panic(err)
} }
} else { default:
// even though the root command or packages command is NOT being run, we still need default bindings // even though the root command or packages command is NOT being run, we still need default bindings
// such that application config parsing passes. // such that application config parsing passes.
if err = bindPackagesConfigOptions(packagesCmd.Flags()); err != nil { if err = bindPackagesConfigOptions(packagesCmd.Flags()); err != nil {

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"sync"
"github.com/anchore/stereoscope" "github.com/anchore/stereoscope"
"github.com/anchore/syft/internal" "github.com/anchore/syft/internal"
@ -13,7 +14,7 @@ import (
"github.com/anchore/syft/internal/formats" "github.com/anchore/syft/internal/formats"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui" "github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/syft" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/event" "github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/format" "github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/sbom"
@ -238,6 +239,12 @@ func packagesExecWorker(userInput string) <-chan error {
go func() { go func() {
defer close(errs) defer close(errs)
tasks, err := tasks()
if err != nil {
errs <- err
return
}
f := formats.ByOption(packagesPresenterOpt) f := formats.ByOption(packagesPresenterOpt)
if f == nil { if f == nil {
errs <- fmt.Errorf("unknown format: %s", packagesPresenterOpt) errs <- fmt.Errorf("unknown format: %s", packagesPresenterOpt)
@ -255,23 +262,24 @@ func packagesExecWorker(userInput string) <-chan error {
defer cleanup() defer cleanup()
} }
catalog, relationships, d, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt) s := sbom.SBOM{
if err != nil {
errs <- fmt.Errorf("failed to catalog input: %w", err)
return
}
sbomResult := sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: catalog,
Distro: d,
},
Relationships: relationships,
Source: src.Metadata, Source: src.Metadata,
} }
var relationships []<-chan artifact.Relationship
for _, task := range tasks {
c := make(chan artifact.Relationship)
relationships = append(relationships, c)
go runTask(task, &s.Artifacts, src, c, errs)
}
for relationship := range mergeRelationships(relationships...) {
s.Relationships = append(s.Relationships, relationship)
}
if appConfig.Anchore.Host != "" { if appConfig.Anchore.Host != "" {
if err := runPackageSbomUpload(src, sbomResult); err != nil { if err := runPackageSbomUpload(src, s); err != nil {
errs <- err errs <- err
return return
} }
@ -279,12 +287,33 @@ func packagesExecWorker(userInput string) <-chan error {
bus.Publish(partybus.Event{ bus.Publish(partybus.Event{
Type: event.PresenterReady, Type: event.PresenterReady,
Value: f.Presenter(sbomResult), Value: f.Presenter(s),
}) })
}() }()
return errs return errs
} }
func mergeRelationships(cs ...<-chan artifact.Relationship) <-chan artifact.Relationship {
var wg sync.WaitGroup
var relationships = make(chan artifact.Relationship)
wg.Add(len(cs))
for _, c := range cs {
go func(c <-chan artifact.Relationship) {
for n := range c {
relationships <- n
}
wg.Done()
}(c)
}
go func() {
wg.Wait()
close(relationships)
}()
return relationships
}
func runPackageSbomUpload(src *source.Source, s sbom.SBOM) error { func runPackageSbomUpload(src *source.Source, s sbom.SBOM) error {
log.Infof("uploading results to %s", appConfig.Anchore.Host) log.Infof("uploading results to %s", appConfig.Anchore.Host)

View File

@ -2,9 +2,10 @@ package cmd
import ( import (
"fmt" "fmt"
"sync" "os"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/gookit/color"
"github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/sbom"
@ -23,6 +24,8 @@ import (
const powerUserExample = ` {{.appName}} {{.command}} <image> const powerUserExample = ` {{.appName}} {{.command}} <image>
DEPRECATED - THIS COMMAND WILL BE REMOVED in v1.0.0
Only image sources are supported (e.g. docker: , docker-archive: , oci: , etc.), the directory source (dir:) is not supported. Only image sources are supported (e.g. docker: , docker-archive: , oci: , etc.), the directory source (dir:) is not supported.
All behavior is controlled via application configuration and environment variables (see https://github.com/anchore/syft#configuration) All behavior is controlled via application configuration and environment variables (see https://github.com/anchore/syft#configuration)
@ -76,6 +79,10 @@ func powerUserExec(_ *cobra.Command, args []string) error {
if err := closer(); err != nil { if err := closer(); err != nil {
log.Warnf("unable to write to report destination: %+v", err) log.Warnf("unable to write to report destination: %+v", err)
} }
// inform user at end of run that command will be removed
deprecated := color.Style{color.Red, color.OpBold}.Sprint("DEPRECATED: This command will be removed in v1.0.0")
fmt.Fprintln(os.Stderr, deprecated)
}() }()
if err != nil { if err != nil {
@ -95,7 +102,11 @@ func powerUserExecWorker(userInput string) <-chan error {
go func() { go func() {
defer close(errs) defer close(errs)
tasks, err := powerUserTasks() appConfig.Secrets.Cataloger.Enabled = true
appConfig.FileMetadata.Cataloger.Enabled = true
appConfig.FileContents.Cataloger.Enabled = true
appConfig.FileClassification.Cataloger.Enabled = true
tasks, err := tasks()
if err != nil { if err != nil {
errs <- err errs <- err
return return
@ -116,15 +127,15 @@ func powerUserExecWorker(userInput string) <-chan error {
Source: src.Metadata, Source: src.Metadata,
} }
var results []<-chan artifact.Relationship var relationships []<-chan artifact.Relationship
for _, task := range tasks { for _, task := range tasks {
c := make(chan artifact.Relationship) c := make(chan artifact.Relationship)
results = append(results, c) relationships = append(relationships, c)
go runTask(task, &s.Artifacts, src, c, errs) go runTask(task, &s.Artifacts, src, c, errs)
} }
for relationship := range mergeResults(results...) { for relationship := range mergeRelationships(relationships...) {
s.Relationships = append(s.Relationships, relationship) s.Relationships = append(s.Relationships, relationship)
} }
@ -133,40 +144,6 @@ func powerUserExecWorker(userInput string) <-chan error {
Value: poweruser.NewJSONPresenter(s, *appConfig), Value: poweruser.NewJSONPresenter(s, *appConfig),
}) })
}() }()
return errs return errs
} }
func runTask(t powerUserTask, a *sbom.Artifacts, src *source.Source, c chan<- artifact.Relationship, errs chan<- error) {
defer close(c)
relationships, err := t(a, src)
if err != nil {
errs <- err
return
}
for _, relationship := range relationships {
c <- relationship
}
}
func mergeResults(cs ...<-chan artifact.Relationship) <-chan artifact.Relationship {
var wg sync.WaitGroup
var results = make(chan artifact.Relationship)
wg.Add(len(cs))
for _, c := range cs {
go func(c <-chan artifact.Relationship) {
for n := range c {
results <- n
}
wg.Done()
}(c)
}
go func() {
wg.Wait()
close(results)
}()
return results
}

View File

@ -4,27 +4,25 @@ import (
"crypto" "crypto"
"fmt" "fmt"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft" "github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
) )
type powerUserTask func(*sbom.Artifacts, *source.Source) ([]artifact.Relationship, error) type task func(*sbom.Artifacts, *source.Source) ([]artifact.Relationship, error)
func powerUserTasks() ([]powerUserTask, error) { func tasks() ([]task, error) {
var tasks []powerUserTask var tasks []task
generators := []func() (powerUserTask, error){ generators := []func() (task, error){
catalogPackagesTask, generateCatalogPackagesTask,
catalogFileMetadataTask, generateCatalogFileMetadataTask,
catalogFileDigestsTask, generateCatalogFileDigestsTask,
catalogSecretsTask, generateCatalogSecretsTask,
catalogFileClassificationsTask, generateCatalogFileClassificationsTask,
catalogContentsTask, generateCatalogContentsTask,
} }
for _, generator := range generators { for _, generator := range generators {
@ -32,6 +30,7 @@ func powerUserTasks() ([]powerUserTask, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if task != nil { if task != nil {
tasks = append(tasks, task) tasks = append(tasks, task)
} }
@ -40,7 +39,7 @@ func powerUserTasks() ([]powerUserTask, error) {
return tasks, nil return tasks, nil
} }
func catalogPackagesTask() (powerUserTask, error) { func generateCatalogPackagesTask() (task, error) {
if !appConfig.Package.Cataloger.Enabled { if !appConfig.Package.Cataloger.Enabled {
return nil, nil return nil, nil
} }
@ -60,7 +59,7 @@ func catalogPackagesTask() (powerUserTask, error) {
return task, nil return task, nil
} }
func catalogFileMetadataTask() (powerUserTask, error) { func generateCatalogFileMetadataTask() (task, error) {
if !appConfig.FileMetadata.Cataloger.Enabled { if !appConfig.FileMetadata.Cataloger.Enabled {
return nil, nil return nil, nil
} }
@ -84,7 +83,7 @@ func catalogFileMetadataTask() (powerUserTask, error) {
return task, nil return task, nil
} }
func catalogFileDigestsTask() (powerUserTask, error) { func generateCatalogFileDigestsTask() (task, error) {
if !appConfig.FileMetadata.Cataloger.Enabled { if !appConfig.FileMetadata.Cataloger.Enabled {
return nil, nil return nil, nil
} }
@ -130,7 +129,7 @@ func catalogFileDigestsTask() (powerUserTask, error) {
return task, nil return task, nil
} }
func catalogSecretsTask() (powerUserTask, error) { func generateCatalogSecretsTask() (task, error) {
if !appConfig.Secrets.Cataloger.Enabled { if !appConfig.Secrets.Cataloger.Enabled {
return nil, nil return nil, nil
} }
@ -162,7 +161,7 @@ func catalogSecretsTask() (powerUserTask, error) {
return task, nil return task, nil
} }
func catalogFileClassificationsTask() (powerUserTask, error) { func generateCatalogFileClassificationsTask() (task, error) {
if !appConfig.FileClassification.Cataloger.Enabled { if !appConfig.FileClassification.Cataloger.Enabled {
return nil, nil return nil, nil
} }
@ -190,7 +189,7 @@ func catalogFileClassificationsTask() (powerUserTask, error) {
return task, nil return task, nil
} }
func catalogContentsTask() (powerUserTask, error) { func generateCatalogContentsTask() (task, error) {
if !appConfig.FileContents.Cataloger.Enabled { if !appConfig.FileContents.Cataloger.Enabled {
return nil, nil return nil, nil
} }
@ -216,3 +215,17 @@ func catalogContentsTask() (powerUserTask, error) {
return task, nil return task, nil
} }
func runTask(t task, a *sbom.Artifacts, src *source.Source, c chan<- artifact.Relationship, errs chan<- error) {
defer close(c)
relationships, err := t(a, src)
if err != nil {
errs <- err
return
}
for _, relationship := range relationships {
c <- relationship
}
}

View File

@ -17,6 +17,8 @@ import (
var ErrApplicationConfigNotFound = fmt.Errorf("application config not found") var ErrApplicationConfigNotFound = fmt.Errorf("application config not found")
var catalogerEnabledDefault = false
type defaultValueLoader interface { type defaultValueLoader interface {
loadDefaultValues(*viper.Viper) loadDefaultValues(*viper.Viper)
} }
@ -44,6 +46,11 @@ type Application struct {
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"` Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
} }
// PowerUserCatalogerEnabledDefault switches all catalogers to be enabled when running power-user command
func PowerUserCatalogerEnabledDefault() {
catalogerEnabledDefault = true
}
func newApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions) *Application { func newApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions) *Application {
config := &Application{ config := &Application{
CliOptions: cliOpts, CliOptions: cliOpts,

View File

@ -10,7 +10,7 @@ type fileClassification struct {
} }
func (cfg fileClassification) loadDefaultValues(v *viper.Viper) { func (cfg fileClassification) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-classification.cataloger.enabled", true) v.SetDefault("file-classification.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-classification.cataloger.scope", source.SquashedScope) v.SetDefault("file-classification.cataloger.scope", source.SquashedScope)
} }

View File

@ -13,7 +13,7 @@ type fileContents struct {
} }
func (cfg fileContents) loadDefaultValues(v *viper.Viper) { func (cfg fileContents) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-contents.cataloger.enabled", true) v.SetDefault("file-contents.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-contents.cataloger.scope", source.SquashedScope) v.SetDefault("file-contents.cataloger.scope", source.SquashedScope)
v.SetDefault("file-contents.skip-files-above-size", 1*file.MB) v.SetDefault("file-contents.skip-files-above-size", 1*file.MB)
v.SetDefault("file-contents.globs", []string{}) v.SetDefault("file-contents.globs", []string{})

View File

@ -11,7 +11,7 @@ type FileMetadata struct {
} }
func (cfg FileMetadata) loadDefaultValues(v *viper.Viper) { func (cfg FileMetadata) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-metadata.cataloger.enabled", true) v.SetDefault("file-metadata.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-metadata.cataloger.scope", source.SquashedScope) v.SetDefault("file-metadata.cataloger.scope", source.SquashedScope)
v.SetDefault("file-metadata.digests", []string{"sha256"}) v.SetDefault("file-metadata.digests", []string{"sha256"})
} }

View File

@ -15,7 +15,7 @@ type secrets struct {
} }
func (cfg secrets) loadDefaultValues(v *viper.Viper) { func (cfg secrets) loadDefaultValues(v *viper.Viper) {
v.SetDefault("secrets.cataloger.enabled", true) v.SetDefault("secrets.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("secrets.cataloger.scope", source.AllLayersScope) v.SetDefault("secrets.cataloger.scope", source.AllLayersScope)
v.SetDefault("secrets.reveal-values", false) v.SetDefault("secrets.reveal-values", false)
v.SetDefault("secrets.skip-files-above-size", 1*file.MB) v.SetDefault("secrets.skip-files-above-size", 1*file.MB)

View File

@ -11,8 +11,6 @@ import (
"golang.org/x/term" "golang.org/x/term"
) )
// TODO: build tags to exclude options from windows
// Select is responsible for determining the specific UI function given select user option, the current platform // Select is responsible for determining the specific UI function given select user option, the current platform
// config values, and environment status (such as a TTY being present). The first UI in the returned slice of UIs // config values, and environment status (such as a TTY being present). The first UI in the returned slice of UIs
// is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there // is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there