diff --git a/cmd/root.go b/cmd/root.go index af02fd981..3471521dd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -68,7 +68,17 @@ func startWorker(userInput string) <-chan error { } } - catalog, scope, distro, err := syft.Catalog(userInput, appConfig.ScopeOpt) + config := syft.Config{ + PluginDirectory: appConfig.Plugins.Directory, + } + + client, err := syft.NewClient(config) + if err != nil { + errs <- fmt.Errorf("could not initialize syft: %+v", err) + return + } + + catalog, scope, distro, err := client.Catalog(userInput, appConfig.ScopeOpt) if err != nil { errs <- fmt.Errorf("failed to catalog input: %+v", err) return diff --git a/syft/cataloger/cataloger.go b/syft/cataloger/cataloger.go index fb1136644..11f7ec179 100644 --- a/syft/cataloger/cataloger.go +++ b/syft/cataloger/cataloger.go @@ -34,6 +34,7 @@ type Cataloger interface { // TODO: add "IterationNeeded" error to indicate to the driver to continue with another Select/Catalog pass } +// All returns a slice of all locally defined catalogers (defined in child packages). func BuiltIn() []Cataloger { return []Cataloger{ dpkg.New(), @@ -46,8 +47,3 @@ func BuiltIn() []Cataloger { javascript.New(), } } - -// All returns a slice of all locally defined catalogers (defined in child packages). -func All() []Cataloger { - return BuiltIn() -} diff --git a/syft/lib.go b/syft/client.go similarity index 65% rename from syft/lib.go rename to syft/client.go index b74bf3efe..3edd0a52b 100644 --- a/syft/lib.go +++ b/syft/client.go @@ -10,13 +10,35 @@ import ( "github.com/anchore/syft/syft/distro" "github.com/anchore/syft/syft/logger" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/plugin" "github.com/anchore/syft/syft/scope" "github.com/wagoodman/go-partybus" ) +type Config struct { + PluginDirectory string +} + +type Client struct { + config Config + plugins *plugin.Repository +} + +func NewClient(config Config) (Client, error) { + plugins, err := plugin.NewRepositoryFromDirectory(config.PluginDirectory) + if err != nil { + return Client{}, err + } + + return Client{ + config: config, + plugins: plugins, + }, nil +} + // Catalog the given image from a particular perspective (e.g. squashed scope, all-layers scope). Returns the discovered // set of packages, the identified Linux distribution, and the scope object used to wrap the data source. -func Catalog(userInput string, scoptOpt scope.Option) (*pkg.Catalog, *scope.Scope, *distro.Distro, error) { +func (c *Client) Catalog(userInput string, scoptOpt scope.Option) (*pkg.Catalog, *scope.Scope, *distro.Distro, error) { log.Info("cataloging image") s, cleanup, err := scope.NewScope(userInput, scoptOpt) defer cleanup() @@ -24,9 +46,9 @@ func Catalog(userInput string, scoptOpt scope.Option) (*pkg.Catalog, *scope.Scop return nil, nil, nil, err } - d := IdentifyDistro(s) + d := c.IdentifyDistro(s) - catalog, err := CatalogFromScope(s) + catalog, err := c.CatalogFromScope(s) if err != nil { return nil, nil, nil, err } @@ -36,7 +58,7 @@ func Catalog(userInput string, scoptOpt scope.Option) (*pkg.Catalog, *scope.Scop // IdentifyDistro attempts to discover what the underlying Linux distribution may be from the available flat files // provided by the given scope object. If results are inconclusive a "UnknownDistro" Type is returned. -func IdentifyDistro(s scope.Scope) distro.Distro { +func (c *Client) IdentifyDistro(s scope.Scope) distro.Distro { d := distro.Identify(s.Resolver) if d.Type != distro.UnknownDistroType { log.Infof("identified distro: %s", d.String()) @@ -47,9 +69,15 @@ func IdentifyDistro(s scope.Scope) distro.Distro { } // Catalog the given scope, which may represent a container image or filesystem. Returns the discovered set of packages. -func CatalogFromScope(s scope.Scope) (*pkg.Catalog, error) { +func (c *Client) CatalogFromScope(s scope.Scope) (*pkg.Catalog, error) { log.Info("building the catalog") - return cataloger.Catalog(s.Resolver, cataloger.All()...) + pluginCatalogers, deactivatePluginsFn, err := c.plugins.ActivateCatalogers() + defer deactivatePluginsFn() + if err != nil { + return nil, err + } + + return cataloger.Catalog(s.Resolver, append(pluginCatalogers, cataloger.BuiltIn()...)...) } // SetLogger sets the logger object used for all syft logging calls. diff --git a/syft/plugin/cataloger_plugin.go b/syft/plugin/cataloger_plugin.go index c092fb49b..0d1a615ac 100644 --- a/syft/plugin/cataloger_plugin.go +++ b/syft/plugin/cataloger_plugin.go @@ -14,9 +14,7 @@ import ( var _ plugin.GRPCPlugin = &CatalogerPlugin{} // Cataloger is the interface exposed as a plugin. -type Cataloger interface { - cataloger.Cataloger -} +type Cataloger cataloger.Cataloger // CatalogerPlugin is the implementation of plugin.Plugin that is served/consumed. type CatalogerPlugin struct { diff --git a/syft/plugin/collection.go b/syft/plugin/collection.go deleted file mode 100644 index 9d96c6070..000000000 --- a/syft/plugin/collection.go +++ /dev/null @@ -1,7 +0,0 @@ -package plugin - -// repository -type Collection struct { - byType map[Type]Plugin - byName map[string]Plugin -} diff --git a/syft/plugin/discover.go b/syft/plugin/discover.go deleted file mode 100644 index fd8432d49..000000000 --- a/syft/plugin/discover.go +++ /dev/null @@ -1,43 +0,0 @@ -package plugin - -import "path/filepath" - -func Discover(dir string, pluginTypes ...Type) ([]Plugin, error) { - var err error - - if len(pluginTypes) == 0 { - pluginTypes = AllTypes - } - - if !filepath.IsAbs(dir) { - dir, err = filepath.Abs(dir) - if err != nil { - return nil, err - } - } - - var plugins []Plugin - - for _, pluginType := range pluginTypes { - // look into a sub dir named by the plugin type - searchDir := filepath.Join(dir, pluginType.String()) - - paths, err := filepath.Glob(filepath.Join(searchDir, "*")) - if err != nil { - return nil, err - } - - for _, path := range paths { - // TODO: should we use a config for some of this? - plugins = append(plugins, NewPlugin(Config{ - // TODO: should the name be org/name instead of just name? this implies changing the dir storage too - Name: filepath.Base(path), - Type: pluginType, - Command: path, - Args: nil, // TODO - Env: nil, // TODO - })) - } - } - return plugins, nil -} diff --git a/syft/plugin/repository.go b/syft/plugin/repository.go new file mode 100644 index 000000000..fdad964d8 --- /dev/null +++ b/syft/plugin/repository.go @@ -0,0 +1,112 @@ +package plugin + +import ( + "fmt" + "github.com/anchore/syft/syft/cataloger" + "path/filepath" +) + +type Repository struct { + byType map[Type][]Plugin +} + +func NewRepository() *Repository { + return &Repository{ + byType: make(map[Type][]Plugin), + } +} + +func NewRepositoryFromDirectory(dir string) (*Repository, error) { + repo := NewRepository() + if dir == "" { + return repo, nil + } + + return repo, repo.AddFromDirectory(dir) +} + +func (c *Repository) Add(plugin Plugin) { + c.byType[plugin.Config.Type] = append(c.byType[plugin.Config.Type], plugin) +} + +func (c *Repository) AddFromDirectory(dir string, pluginTypes ...Type) error { + plugins, err := Discover(dir, pluginTypes...) + if err != nil { + return err + } + for _, plugin := range plugins { + c.Add(plugin) + } + return nil +} + +func (c *Repository) Get(pluginType Type) []Plugin { + return c.byType[pluginType] +} + +func (c *Repository) ActivateCatalogers() ([]cataloger.Cataloger, func(), error) { + var result []cataloger.Cataloger + var plugins []Plugin + var deactivateFn = func() { + for _, plugin := range plugins { + // TODO: handle error by log + plugin.Stop() + } + } + + for _, plugin := range c.Get(TypeCataloger) { + raw, err := plugin.Start() + if err != nil { + return nil, deactivateFn, err + } + plugins = append(plugins, plugin) + + theCataloger, ok := raw.(cataloger.Cataloger) + if !ok { + return nil, deactivateFn, fmt.Errorf("activation of cataloger did not return a cataloger object (name=%s)", plugin.Config.Name) + } + + result = append(result, theCataloger) + } + return result, deactivateFn, nil +} + +func Discover(dir string, pluginTypes ...Type) ([]Plugin, error) { + var err error + + if len(pluginTypes) == 0 { + pluginTypes = AllTypes + } + + if !filepath.IsAbs(dir) { + dir, err = filepath.Abs(dir) + if err != nil { + return nil, err + } + } + + var plugins []Plugin + + for _, pluginType := range pluginTypes { + // look into a sub dir named by the plugin type + searchDir := filepath.Join(dir, pluginType.String()) + + paths, err := filepath.Glob(filepath.Join(searchDir, "*")) + if err != nil { + return nil, err + } + + for _, path := range paths { + // TODO: should we use a config for some of this? + plugins = append(plugins, NewPlugin(Config{ + // TODO: should the name be org/name instead of just name? this implies changing the dir storage too + Name: filepath.Base(path), + Type: pluginType, + Command: path, + Args: nil, // TODO + Env: nil, // TODO + })) + } + } + return plugins, nil +}