Add documentation around catalogers, UI elements, and the event bus (#143)

* add basic documentation for catalogers (with refactoring for simplification)

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add docs for catalog parsers, UI, and event bus

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* update bus phrasing

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Alex Goodman 2020-08-12 11:04:39 -04:00 committed by GitHub
parent 5042d371cf
commit 51b9c73c31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 250 additions and 86 deletions

View File

@ -1,3 +1,16 @@
/*
Package bus provides access to a singleton instance of an event bus (provided by the calling application). The event bus
is intended to allow for the syft library to publish events which library consumers can subscribe to. These events
can provide static information, but also have an object as a payload for which the consumer can poll for updates.
This is akin to a logger, except instead of only allowing strings to be logged, rich objects that can be interacted with.
Note that the singleton instance is only allowed to publish events and not subscribe to them --this is intentional.
Internal library interactions should continue to use traditional in-execution-path approaches for data sharing
(e.g. function returns and channels) and not depend on bus subscriptions for critical interactions (e.g. one part of the
lib publishes an event and another part of the lib subscribes and reacts to that event). The bus is provided only as a
means for consumers to observe events emitted from the library (such as to provide a rich UI) and not to allow
consumers to augment or otherwise change execution.
*/
package bus package bus
import "github.com/wagoodman/go-partybus" import "github.com/wagoodman/go-partybus"
@ -5,6 +18,8 @@ import "github.com/wagoodman/go-partybus"
var publisher partybus.Publisher var publisher partybus.Publisher
var active bool var active bool
// SetPublisher sets the singleton event bus publisher. This is optional; if no bus is provided, the library will
// behave no differently than if a bus had been provided.
func SetPublisher(p partybus.Publisher) { func SetPublisher(p partybus.Publisher) {
publisher = p publisher = p
if p != nil { if p != nil {
@ -12,6 +27,7 @@ func SetPublisher(p partybus.Publisher) {
} }
} }
// Publish an event onto the bus. If there is no bus set by the calling application, this does nothing.
func Publish(event partybus.Event) { func Publish(event partybus.Event) {
if active { if active {
publisher.Publish(event) publisher.Publish(event)

View File

@ -8,6 +8,8 @@ import (
"github.com/wagoodman/go-partybus" "github.com/wagoodman/go-partybus"
) )
// CatalogerFinishedHandler is a UI function for processing the CatalogerFinished bus event, displaying the catalog
// via the given presenter to stdout.
func CatalogerFinishedHandler(event partybus.Event) error { func CatalogerFinishedHandler(event partybus.Event) error {
// show the report to stdout // show the report to stdout
pres, err := syftEventParsers.ParseCatalogerFinished(event) pres, err := syftEventParsers.ParseCatalogerFinished(event)

View File

@ -1,3 +1,21 @@
/*
Package etui provides an "ephemeral" terminal user interface to display the application state dynamically.
The terminal is placed into raw mode and the cursor is manipulated to allow for a dynamic, multi-line
UI (provided by the jotframe lib), for this reason all other application mechanisms that write to the screen
must be suppressed before starting (such as logs); since bytes in the device and in application memory combine to make
a shared state, bytes coming from elsewhere to the screen will disrupt this state.
This UI is primarily driven off of events from the event bus, creating single-line terminal widgets to represent a
published element on the event bus, typically polling the element for the latest state. This allows for the UI to
control update frequency to the screen, provide "liveness" indications that are interpolated between bus events,
and overall loosely couple the bus events from screen interactions.
By convention, all elements published on the bus should be treated as read-only, and publishers on the bus should
attempt to enforce this when possible by wrapping complex objects with interfaces to prescribe interactions. Also by
convention, each new event that the UI should respond to should be added either in this package as a handler function,
or in the shared ui package as a function on the main handler object. All handler functions should be completed
processing an event before the ETUI exits (coordinated with a sync.WaitGroup)
*/
package etui package etui
import ( import (
@ -18,8 +36,10 @@ import (
"github.com/wagoodman/jotframe/pkg/frame" "github.com/wagoodman/jotframe/pkg/frame"
) )
// TODO: specify per-platform implementations with build tags // TODO: specify per-platform implementations with build tags (needed when windows is supported by syft)
// setupScreen creates a new jotframe object to manage specific screen lines dynamically, preparing the screen device
// as needed (i.e. setting the terminal to raw mode).
func setupScreen(output *os.File) *frame.Frame { func setupScreen(output *os.File) *frame.Frame {
config := frame.Config{ config := frame.Config{
PositionPolicy: frame.PolicyFloatForward, PositionPolicy: frame.PolicyFloatForward,
@ -36,6 +56,8 @@ func setupScreen(output *os.File) *frame.Frame {
} }
// nolint:funlen,gocognit // nolint:funlen,gocognit
// OutputToEphemeralTUI is a UI function that provides a terminal UI experience without a separate, in-application
// screen buffer. All logging is suppressed, buffered to a string, and output after the ETUI has been torn down.
func OutputToEphemeralTUI(workerErrs <-chan error, subscription *partybus.Subscription) error { func OutputToEphemeralTUI(workerErrs <-chan error, subscription *partybus.Subscription) error {
output := os.Stderr output := os.Stderr
@ -84,6 +106,7 @@ eventLoop:
for { for {
select { select {
case err := <-workerErrs: case err := <-workerErrs:
// TODO: we should show errors more explicitly in the ETUI
if err != nil { if err != nil {
return err return err
} }

View File

@ -13,6 +13,7 @@ import (
"github.com/wagoodman/jotframe/pkg/frame" "github.com/wagoodman/jotframe/pkg/frame"
) )
// appUpdateAvailableHandler is a UI handler function to display a new application version to the top of the screen.
func appUpdateAvailableHandler(_ context.Context, fr *frame.Frame, event partybus.Event, _ *sync.WaitGroup) error { func appUpdateAvailableHandler(_ context.Context, fr *frame.Frame, event partybus.Event, _ *sync.WaitGroup) error {
newVersion, err := syftEventParsers.ParseAppUpdateAvailable(event) newVersion, err := syftEventParsers.ParseAppUpdateAvailable(event)
if err != nil { if err != nil {

View File

@ -7,6 +7,7 @@ import (
"github.com/wagoodman/go-partybus" "github.com/wagoodman/go-partybus"
) )
// LoggerUI is a UI function that leverages the displays all application logs to the screen.
func LoggerUI(workerErrs <-chan error, subscription *partybus.Subscription) error { func LoggerUI(workerErrs <-chan error, subscription *partybus.Subscription) error {
events := subscription.Events() events := subscription.Events()
eventLoop: eventLoop:

View File

@ -10,6 +10,8 @@ import (
// TODO: build tags to exclude options from windows // TODO: build tags to exclude options from windows
// 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).
func Select(verbose, quiet bool) UI { func Select(verbose, quiet bool) UI {
var ui UI var ui UI

View File

@ -4,4 +4,6 @@ import (
"github.com/wagoodman/go-partybus" "github.com/wagoodman/go-partybus"
) )
// UI is a function that takes a channel of errors from the main() worker and a event bus subscription and is
// responsible for displaying pertinent events to the user, on the screen or otherwise.
type UI func(<-chan error, *partybus.Subscription) error type UI func(<-chan error, *partybus.Subscription) error

View File

@ -7,22 +7,25 @@ import (
const valueNotProvided = "[not provided]" const valueNotProvided = "[not provided]"
// all variables here are provided as build-time arguments, with clear default values
var version = valueNotProvided var version = valueNotProvided
var gitCommit = valueNotProvided var gitCommit = valueNotProvided
var gitTreeState = valueNotProvided var gitTreeState = valueNotProvided
var buildDate = valueNotProvided var buildDate = valueNotProvided
var platform = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) var platform = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
// Version defines the application version details (generally from build information)
type Version struct { type Version struct {
Version string Version string // application semantic version
GitCommit string GitCommit string // git SHA at build-time
GitTreeState string GitTreeState string // indication of git tree (either "clean" or "dirty") at build-time
BuildDate string BuildDate string // date of the build
GoVersion string GoVersion string // go runtime version at build-time
Compiler string Compiler string // compiler used at build-time
Platform string Platform string // GOOS and GOARCH at build-time
} }
// FromBuild provides all version details
func FromBuild() Version { func FromBuild() Version {
return Version{ return Version{
Version: version, Version: version,

View File

@ -7,11 +7,13 @@ import (
"github.com/anchore/syft/syft/scope" "github.com/anchore/syft/syft/scope"
) )
// Cataloger catalogs pkg.ApkPkg Package Types defined in Alpine DB files.
type Cataloger struct { type Cataloger struct {
cataloger common.GenericCataloger cataloger common.GenericCataloger
} }
func NewCataloger() *Cataloger { // New returns a new Alpine DB cataloger object.
func New() *Cataloger {
globParsers := map[string]common.ParserFn{ globParsers := map[string]common.ParserFn{
"**/lib/apk/db/installed": parseApkDB, "**/lib/apk/db/installed": parseApkDB,
} }
@ -21,14 +23,17 @@ func NewCataloger() *Cataloger {
} }
} }
// Name returns a string that uniquely describes this cataloger.
func (a *Cataloger) Name() string { func (a *Cataloger) Name() string {
return "apkdb-cataloger" return "apkdb-cataloger"
} }
// SelectFiles returns a set of discovered Alpine DB files from the user content source.
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference { func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
return a.cataloger.SelectFiles(resolver) return a.cataloger.SelectFiles(resolver)
} }
// Catalog returns the Packages indexed from all Alpine DB files discovered.
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) { func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
return a.cataloger.Catalog(contents, a.Name()) return a.cataloger.Catalog(contents, a.Name())
} }

View File

@ -9,10 +9,16 @@ import (
"strings" "strings"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
) )
// integrity check
var _ common.ParserFn = parseApkDB
// parseApkDb parses individual packages from a given Alpine DB file. For more information on specific fields
// see https://wiki.alpinelinux.org/wiki/Apk_spec .
func parseApkDB(_ string, reader io.Reader) ([]pkg.Package, error) { func parseApkDB(_ string, reader io.Reader) ([]pkg.Package, error) {
packages := make([]pkg.Package, 0) packages := make([]pkg.Package, 0)
@ -55,6 +61,7 @@ func parseApkDB(_ string, reader io.Reader) ([]pkg.Package, error) {
} }
// nolint:funlen // nolint:funlen
// parseApkDBEntry reads and parses a single pkg.ApkMetadata element from the stream, returning nil if their are no more entries.
func parseApkDBEntry(reader io.Reader) (*pkg.ApkMetadata, error) { func parseApkDBEntry(reader io.Reader) (*pkg.ApkMetadata, error) {
var entry pkg.ApkMetadata var entry pkg.ApkMetadata
pkgFields := make(map[string]interface{}) pkgFields := make(map[string]interface{})

View File

@ -7,11 +7,13 @@ import (
"github.com/anchore/syft/syft/scope" "github.com/anchore/syft/syft/scope"
) )
// Cataloger catalogs pkg.GemPkg Package Types defined in Bundler Gemfile.lock files.
type Cataloger struct { type Cataloger struct {
cataloger common.GenericCataloger cataloger common.GenericCataloger
} }
func NewCataloger() *Cataloger { // New returns a new Bundler cataloger object.
func New() *Cataloger {
globParsers := map[string]common.ParserFn{ globParsers := map[string]common.ParserFn{
"**/Gemfile.lock": parseGemfileLockEntries, "**/Gemfile.lock": parseGemfileLockEntries,
} }
@ -21,14 +23,17 @@ func NewCataloger() *Cataloger {
} }
} }
// Name returns a string that uniquely describes this cataloger.
func (a *Cataloger) Name() string { func (a *Cataloger) Name() string {
return "bundler-cataloger" return "bundler-cataloger"
} }
// SelectFiles returns a set of discovered Gemfile.lock files from the user content source.
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference { func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
return a.cataloger.SelectFiles(resolver) return a.cataloger.SelectFiles(resolver)
} }
// Catalog returns the Packages indexed from all Gemfile.lock files discovered.
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) { func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
return a.cataloger.Catalog(contents, a.Name()) return a.cataloger.Catalog(contents, a.Name())
} }

View File

@ -6,11 +6,16 @@ import (
"strings" "strings"
"github.com/anchore/syft/internal" "github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
) )
// integrity check
var _ common.ParserFn = parseGemfileLockEntries
var sectionsOfInterest = internal.NewStringSetFromSlice([]string{"GEM"}) var sectionsOfInterest = internal.NewStringSetFromSlice([]string{"GEM"})
// parseGemfileLockEntries is a parser function for Gemfile.lock contents, returning all Gems discovered.
func parseGemfileLockEntries(_ string, reader io.Reader) ([]pkg.Package, error) { func parseGemfileLockEntries(_ string, reader io.Reader) ([]pkg.Package, error) {
pkgs := make([]pkg.Package, 0) pkgs := make([]pkg.Package, 0)
scanner := bufio.NewScanner(reader) scanner := bufio.NewScanner(reader)

View File

@ -4,14 +4,6 @@ import (
"github.com/anchore/stereoscope/pkg/file" "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/internal/bus" "github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/cataloger/apkdb"
"github.com/anchore/syft/syft/cataloger/bundler"
"github.com/anchore/syft/syft/cataloger/dpkg"
golang "github.com/anchore/syft/syft/cataloger/golang"
"github.com/anchore/syft/syft/cataloger/java"
"github.com/anchore/syft/syft/cataloger/javascript"
"github.com/anchore/syft/syft/cataloger/python"
"github.com/anchore/syft/syft/cataloger/rpmdb"
"github.com/anchore/syft/syft/event" "github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/scope" "github.com/anchore/syft/syft/scope"
@ -20,54 +12,14 @@ import (
"github.com/wagoodman/go-progress" "github.com/wagoodman/go-progress"
) )
var controllerInstance controller // Monitor provides progress-related data for observing the progress of a Catalog() call (published on the event bus).
func init() {
controllerInstance = newController()
}
func Catalogers() []string {
c := make([]string, len(controllerInstance.catalogers))
for idx, catalog := range controllerInstance.catalogers {
c[idx] = catalog.Name()
}
return c
}
func Catalog(s scope.Resolver) (*pkg.Catalog, error) {
return controllerInstance.catalog(s)
}
type controller struct {
catalogers []Cataloger
}
func newController() controller {
ctrlr := controller{
catalogers: make([]Cataloger, 0),
}
ctrlr.add(dpkg.NewCataloger())
ctrlr.add(bundler.NewCataloger())
ctrlr.add(python.NewCataloger())
ctrlr.add(rpmdb.NewCataloger())
ctrlr.add(java.NewCataloger())
ctrlr.add(apkdb.NewCataloger())
ctrlr.add(golang.NewCataloger())
ctrlr.add(javascript.NewCataloger())
return ctrlr
}
func (c *controller) add(a Cataloger) {
log.Debugf("adding cataloger: %s", a.Name())
c.catalogers = append(c.catalogers, a)
}
type Monitor struct { type Monitor struct {
FilesProcessed progress.Monitorable FilesProcessed progress.Monitorable // the number of files selected and contents analyzed from all registered catalogers
PackagesDiscovered progress.Monitorable PackagesDiscovered progress.Monitorable // the number of packages discovered from all registered catalogers
} }
func (c *controller) trackCataloger() (*progress.Manual, *progress.Manual) { // newMonitor creates a new Monitor object and publishes the object on the bus as a CatalogerStarted event.
func newMonitor() (*progress.Manual, *progress.Manual) {
filesProcessed := progress.Manual{} filesProcessed := progress.Manual{}
packagesDiscovered := progress.Manual{} packagesDiscovered := progress.Manual{}
@ -81,20 +33,25 @@ func (c *controller) trackCataloger() (*progress.Manual, *progress.Manual) {
return &filesProcessed, &packagesDiscovered return &filesProcessed, &packagesDiscovered
} }
func (c *controller) catalog(s scope.Resolver) (*pkg.Catalog, error) { // Catalog a given scope (container image or filesystem) with the given catalogers, returning all discovered packages.
// In order to efficiently retrieve contents from a underlying container image the content fetch requests are
// done in bulk. Specifically, all files of interest are collected from each catalogers and accumulated into a single
// request.
func Catalog(s scope.Resolver, catalogers ...Cataloger) (*pkg.Catalog, error) {
catalog := pkg.NewCatalog() catalog := pkg.NewCatalog()
fileSelection := make([]file.Reference, 0) fileSelection := make([]file.Reference, 0)
filesProcessed, packagesDiscovered := c.trackCataloger() filesProcessed, packagesDiscovered := newMonitor()
// ask catalogers for files to extract from the image tar // ask catalogers for files to extract from the image tar
for _, a := range c.catalogers { for _, a := range catalogers {
fileSelection = append(fileSelection, a.SelectFiles(s)...) fileSelection = append(fileSelection, a.SelectFiles(s)...)
log.Debugf("cataloger '%s' selected '%d' files", a.Name(), len(fileSelection)) log.Debugf("cataloger '%s' selected '%d' files", a.Name(), len(fileSelection))
filesProcessed.N += int64(len(fileSelection)) filesProcessed.N += int64(len(fileSelection))
} }
// fetch contents for requested selection by catalogers // fetch contents for requested selection by catalogers
// TODO: we should consider refactoring to return a set of io.Readers instead of the full contents themselves (allow for optional buffering).
contents, err := s.MultipleFileContentsByRef(fileSelection...) contents, err := s.MultipleFileContentsByRef(fileSelection...)
if err != nil { if err != nil {
return nil, err return nil, err
@ -102,7 +59,7 @@ func (c *controller) catalog(s scope.Resolver) (*pkg.Catalog, error) {
// perform analysis, accumulating errors for each failed analysis // perform analysis, accumulating errors for each failed analysis
var errs error var errs error
for _, a := range c.catalogers { for _, a := range catalogers {
// TODO: check for multiple rounds of analyses by Iterate error // TODO: check for multiple rounds of analyses by Iterate error
packages, err := a.Catalog(contents) packages, err := a.Catalog(contents)
if err != nil { if err != nil {

View File

@ -1,16 +1,48 @@
/*
Package cataloger provides the ability to process files from a container image or file system and discover packages
(e.g. gems, wheels, jars, rpms, debs, etc.). Specifically, this package contains both a catalog function to utilize all
catalogers defined in child packages as well as the interface definition to implement a cataloger.
*/
package cataloger package cataloger
import ( import (
"github.com/anchore/stereoscope/pkg/file" "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/syft/cataloger/apkdb"
"github.com/anchore/syft/syft/cataloger/bundler"
"github.com/anchore/syft/syft/cataloger/dpkg"
"github.com/anchore/syft/syft/cataloger/golang"
"github.com/anchore/syft/syft/cataloger/java"
"github.com/anchore/syft/syft/cataloger/javascript"
"github.com/anchore/syft/syft/cataloger/python"
"github.com/anchore/syft/syft/cataloger/rpmdb"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/scope" "github.com/anchore/syft/syft/scope"
) )
// Cataloger describes behavior for an object to participate in parsing container image or file system
// contents for the purpose of discovering Packages. Each concrete implementation should focus on discovering Packages
// for a specific Package Type or ecosystem.
type Cataloger interface { type Cataloger interface {
// Name returns a string that uniquely describes a cataloger
Name() string Name() string
// TODO: add ID / Name for catalog for uniquely identifying this cataloger type // SelectFiles discovers and returns specific files that the cataloger would like to inspect the contents of.
SelectFiles(scope.FileResolver) []file.Reference SelectFiles(scope.FileResolver) []file.Reference
// NOTE: one of the errors which is returned is "IterationNeeded", which indicates to the driver to // Catalog is given the file contents and should return any discovered Packages after analyzing the contents.
// continue with another Select/Catalog pass
Catalog(map[file.Reference]string) ([]pkg.Package, error) Catalog(map[file.Reference]string) ([]pkg.Package, error)
// TODO: add "IterationNeeded" error to indicate to the driver to continue with another Select/Catalog pass
// TODO: we should consider refactoring to return a set of io.Readers instead of the full contents themselves (allow for optional buffering).
}
// All returns a slice of all locally defined catalogers (defined in child packages).
func All() []Cataloger {
return []Cataloger{
dpkg.New(),
bundler.New(),
python.New(),
rpmdb.New(),
java.New(),
apkdb.New(),
golang.New(),
javascript.New(),
}
} }

View File

@ -7,11 +7,13 @@ import (
"github.com/anchore/syft/syft/scope" "github.com/anchore/syft/syft/scope"
) )
// Cataloger catalogs pkg.DebPkg Package Types defined in DPKG status files.
type Cataloger struct { type Cataloger struct {
cataloger common.GenericCataloger cataloger common.GenericCataloger
} }
func NewCataloger() *Cataloger { // New returns a new Deb package cataloger object.
func New() *Cataloger {
globParsers := map[string]common.ParserFn{ globParsers := map[string]common.ParserFn{
"**/var/lib/dpkg/status": parseDpkgStatus, "**/var/lib/dpkg/status": parseDpkgStatus,
} }
@ -21,14 +23,17 @@ func NewCataloger() *Cataloger {
} }
} }
// Name returns a string that uniquely describes this cataloger.
func (a *Cataloger) Name() string { func (a *Cataloger) Name() string {
return "dpkg-cataloger" return "dpkg-cataloger"
} }
// SelectFiles returns a set of discovered DPKG status files from the user content source.
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference { func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
return a.cataloger.SelectFiles(resolver) return a.cataloger.SelectFiles(resolver)
} }
// Catalog returns the Packages indexed from all DPKG status files discovered.
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) { func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
return a.cataloger.Catalog(contents, a.Name()) return a.cataloger.Catalog(contents, a.Name())
} }

View File

@ -6,12 +6,17 @@ import (
"io" "io"
"strings" "strings"
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
) )
// integrity check
var _ common.ParserFn = parseDpkgStatus
var errEndOfPackages = fmt.Errorf("no more packages to read") var errEndOfPackages = fmt.Errorf("no more packages to read")
// parseDpkgStatus is a parser function for Debian DB status contents, returning all Debian packages listed.
func parseDpkgStatus(_ string, reader io.Reader) ([]pkg.Package, error) { func parseDpkgStatus(_ string, reader io.Reader) ([]pkg.Package, error) {
buffedReader := bufio.NewReader(reader) buffedReader := bufio.NewReader(reader)
var packages = make([]pkg.Package, 0) var packages = make([]pkg.Package, 0)
@ -35,6 +40,7 @@ func parseDpkgStatus(_ string, reader io.Reader) ([]pkg.Package, error) {
return packages, nil return packages, nil
} }
// parseDpkgStatusEntry returns an individual Dpkg entry, or returns errEndOfPackages if there are no more packages to parse from the reader.
func parseDpkgStatusEntry(reader *bufio.Reader) (entry pkg.DpkgMetadata, err error) { func parseDpkgStatusEntry(reader *bufio.Reader) (entry pkg.DpkgMetadata, err error) {
dpkgFields := make(map[string]string) dpkgFields := make(map[string]string)
var key string var key string

View File

@ -7,11 +7,13 @@ import (
"github.com/anchore/syft/syft/scope" "github.com/anchore/syft/syft/scope"
) )
// Cataloger catalogs pkg.GoModulePkg Package Types defined in go.mod files.
type Cataloger struct { type Cataloger struct {
cataloger common.GenericCataloger cataloger common.GenericCataloger
} }
func NewCataloger() *Cataloger { // New returns a new Go module cataloger object.
func New() *Cataloger {
globParsers := map[string]common.ParserFn{ globParsers := map[string]common.ParserFn{
"**/go.mod": parseGoMod, "**/go.mod": parseGoMod,
} }
@ -21,14 +23,17 @@ func NewCataloger() *Cataloger {
} }
} }
// Name returns a string that uniquely describes this cataloger.
func (a *Cataloger) Name() string { func (a *Cataloger) Name() string {
return "go-cataloger" return "go-cataloger"
} }
// SelectFiles returns a set of discovered go.mod files from the user content source.
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference { func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
return a.cataloger.SelectFiles(resolver) return a.cataloger.SelectFiles(resolver)
} }
// Catalog returns the Packages indexed from all go.mod files discovered.
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) { func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
return a.cataloger.Catalog(contents, a.Name()) return a.cataloger.Catalog(contents, a.Name())
} }

View File

@ -11,6 +11,7 @@ import (
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
) )
// parseGoMod takes a go.mod and lists all packages discovered.
func parseGoMod(path string, reader io.Reader) ([]pkg.Package, error) { func parseGoMod(path string, reader io.Reader) ([]pkg.Package, error) {
packages := make(map[string]pkg.Package) packages := make(map[string]pkg.Package)

View File

@ -7,9 +7,13 @@ import (
"github.com/anchore/syft/internal" "github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/file" "github.com/anchore/syft/internal/file"
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
) )
// integrity check
var _ common.ParserFn = parseJavaArchive
var archiveFormatGlobs = []string{ var archiveFormatGlobs = []string{
"**/*.jar", "**/*.jar",
"**/*.war", "**/*.war",
@ -28,6 +32,7 @@ type archiveParser struct {
detectNested bool detectNested bool
} }
// parseJavaArchive is a parser function for java archive contents, returning all Java libraries and nested archives.
func parseJavaArchive(virtualPath string, reader io.Reader) ([]pkg.Package, error) { func parseJavaArchive(virtualPath string, reader io.Reader) ([]pkg.Package, error) {
parser, cleanupFn, err := newJavaArchiveParser(virtualPath, reader, true) parser, cleanupFn, err := newJavaArchiveParser(virtualPath, reader, true)
// note: even on error, we should always run cleanup functions // note: even on error, we should always run cleanup functions
@ -38,6 +43,7 @@ func parseJavaArchive(virtualPath string, reader io.Reader) ([]pkg.Package, erro
return parser.parse() return parser.parse()
} }
// uniquePkgKey creates a unique string to identify the given package.
func uniquePkgKey(p *pkg.Package) string { func uniquePkgKey(p *pkg.Package) string {
if p == nil { if p == nil {
return "" return ""
@ -45,6 +51,8 @@ func uniquePkgKey(p *pkg.Package) string {
return fmt.Sprintf("%s|%s", p.Name, p.Version) return fmt.Sprintf("%s|%s", p.Name, p.Version)
} }
// newJavaArchiveParser returns a new java archive parser object for the given archive. Can be configured to discover
// and parse nested archives or ignore them.
func newJavaArchiveParser(virtualPath string, reader io.Reader, detectNested bool) (*archiveParser, func(), error) { func newJavaArchiveParser(virtualPath string, reader io.Reader, detectNested bool) (*archiveParser, func(), error) {
contentPath, archivePath, cleanupFn, err := saveArchiveToTmp(reader) contentPath, archivePath, cleanupFn, err := saveArchiveToTmp(reader)
if err != nil { if err != nil {
@ -67,6 +75,7 @@ func newJavaArchiveParser(virtualPath string, reader io.Reader, detectNested boo
}, cleanupFn, nil }, cleanupFn, nil
} }
// parse the loaded archive and return all packages found.
func (j *archiveParser) parse() ([]pkg.Package, error) { func (j *archiveParser) parse() ([]pkg.Package, error) {
var pkgs = make([]pkg.Package, 0) var pkgs = make([]pkg.Package, 0)
@ -106,6 +115,7 @@ func (j *archiveParser) parse() ([]pkg.Package, error) {
return pkgs, nil return pkgs, nil
} }
// discoverMainPackage parses the root Java manifest used as the parent package to all discovered nested packages.
func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) { func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) {
// search and parse java manifest files // search and parse java manifest files
manifestMatches := j.fileManifest.GlobMatch(manifestPath) manifestMatches := j.fileManifest.GlobMatch(manifestPath)
@ -140,6 +150,8 @@ func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) {
}, nil }, nil
} }
// discoverPkgsFromPomProperties parses Maven POM properties for a given parent package, returning all listed Java packages found and
// associating each discovered package to the given parent package.
func (j *archiveParser) discoverPkgsFromPomProperties(parentPkg *pkg.Package) ([]pkg.Package, error) { func (j *archiveParser) discoverPkgsFromPomProperties(parentPkg *pkg.Package) ([]pkg.Package, error) {
var pkgs = make([]pkg.Package, 0) var pkgs = make([]pkg.Package, 0)
parentKey := uniquePkgKey(parentPkg) parentKey := uniquePkgKey(parentPkg)
@ -192,6 +204,8 @@ func (j *archiveParser) discoverPkgsFromPomProperties(parentPkg *pkg.Package) ([
return pkgs, nil return pkgs, nil
} }
// discoverPkgsFromNestedArchives finds Java archives within Java archives, returning all listed Java packages found and
// associating each discovered package to the given parent package.
func (j *archiveParser) discoverPkgsFromNestedArchives(parentPkg *pkg.Package) ([]pkg.Package, error) { func (j *archiveParser) discoverPkgsFromNestedArchives(parentPkg *pkg.Package) ([]pkg.Package, error) {
var pkgs = make([]pkg.Package, 0) var pkgs = make([]pkg.Package, 0)

View File

@ -7,11 +7,13 @@ import (
"github.com/anchore/syft/syft/scope" "github.com/anchore/syft/syft/scope"
) )
// Cataloger catalogs pkg.JavaPkg and pkg.JenkinsPluginPkg Package Types defined in java archive files.
type Cataloger struct { type Cataloger struct {
cataloger common.GenericCataloger cataloger common.GenericCataloger
} }
func NewCataloger() *Cataloger { // New returns a new Java archive cataloger object.
func New() *Cataloger {
globParsers := make(map[string]common.ParserFn) globParsers := make(map[string]common.ParserFn)
for _, pattern := range archiveFormatGlobs { for _, pattern := range archiveFormatGlobs {
globParsers[pattern] = parseJavaArchive globParsers[pattern] = parseJavaArchive
@ -22,14 +24,17 @@ func NewCataloger() *Cataloger {
} }
} }
// Name returns a string that uniquely describes this cataloger.
func (a *Cataloger) Name() string { func (a *Cataloger) Name() string {
return "java-cataloger" return "java-cataloger"
} }
// SelectFiles returns a set of discovered Java archive files from the user content source.
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference { func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
return a.cataloger.SelectFiles(resolver) return a.cataloger.SelectFiles(resolver)
} }
// Catalog returns the Packages indexed from all Java archive files discovered.
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) { func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
return a.cataloger.Catalog(contents, a.Name()) return a.cataloger.Catalog(contents, a.Name())
} }

View File

@ -7,11 +7,13 @@ import (
"github.com/anchore/syft/syft/scope" "github.com/anchore/syft/syft/scope"
) )
// Cataloger catalogs pkg.YarnPkg and pkg.NpmPkg Package Types defined in package-lock.json and yarn.lock files.
type Cataloger struct { type Cataloger struct {
cataloger common.GenericCataloger cataloger common.GenericCataloger
} }
func NewCataloger() *Cataloger { // New returns a new JavaScript cataloger object.
func New() *Cataloger {
globParsers := map[string]common.ParserFn{ globParsers := map[string]common.ParserFn{
"**/package-lock.json": parsePackageLock, "**/package-lock.json": parsePackageLock,
"**/yarn.lock": parseYarnLock, "**/yarn.lock": parseYarnLock,
@ -22,14 +24,17 @@ func NewCataloger() *Cataloger {
} }
} }
// Name returns a string that uniquely describes this cataloger.
func (a *Cataloger) Name() string { func (a *Cataloger) Name() string {
return "npm-cataloger" return "javascript-cataloger"
} }
// SelectFiles returns a set of discovered Javascript ecosystem files from the user content source.
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference { func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
return a.cataloger.SelectFiles(resolver) return a.cataloger.SelectFiles(resolver)
} }
// Catalog returns the Packages indexed from all Javascript ecosystem files discovered.
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) { func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
return a.cataloger.Catalog(contents, a.Name()) return a.cataloger.Catalog(contents, a.Name())
} }

View File

@ -5,15 +5,21 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
) )
// integrity check
var _ common.ParserFn = parsePackageLock
// PackageLock represents a JavaScript package.lock json file
type PackageLock struct { type PackageLock struct {
Requires bool `json:"requires"` Requires bool `json:"requires"`
LockfileVersion int `json:"lockfileVersion"` LockfileVersion int `json:"lockfileVersion"`
Dependencies Dependencies Dependencies map[string]Dependency
} }
// Dependency represents a single package dependency listed in the package.lock json file
type Dependency struct { type Dependency struct {
Version string `json:"version"` Version string `json:"version"`
Resolved string `json:"resolved"` Resolved string `json:"resolved"`
@ -21,8 +27,7 @@ type Dependency struct {
Requires map[string]string Requires map[string]string
} }
type Dependencies map[string]Dependency // parsePackageLock parses a package.lock and returns the discovered JavaScript packages.
func parsePackageLock(_ string, reader io.Reader) ([]pkg.Package, error) { func parsePackageLock(_ string, reader io.Reader) ([]pkg.Package, error) {
packages := make([]pkg.Package, 0) packages := make([]pkg.Package, 0)
dec := json.NewDecoder(reader) dec := json.NewDecoder(reader)

View File

@ -8,9 +8,13 @@ import (
"strings" "strings"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
) )
// integrity check
var _ common.ParserFn = parseYarnLock
var composedNameExp = regexp.MustCompile("^\"(@{1}[^@]+)") var composedNameExp = regexp.MustCompile("^\"(@{1}[^@]+)")
var simpleNameExp = regexp.MustCompile(`^[a-zA-Z\-]+@`) var simpleNameExp = regexp.MustCompile(`^[a-zA-Z\-]+@`)
var versionExp = regexp.MustCompile(`^\W+(version)\W+`) var versionExp = regexp.MustCompile(`^\W+(version)\W+`)

View File

@ -7,14 +7,13 @@ import (
"github.com/anchore/syft/syft/scope" "github.com/anchore/syft/syft/scope"
) )
// Cataloger catalogs pkg.WheelPkg, pkg.EggPkg, and pkg.PythonRequirementsPkg Package Types defined in Python ecosystem files.
type Cataloger struct { type Cataloger struct {
cataloger common.GenericCataloger cataloger common.GenericCataloger
} }
func NewCataloger() *Cataloger { // New returns a new Python cataloger object.
// we want to match on partial dir names func New() *Cataloger {
// /home/user/requests-2.10.0.dist-info/METADATA
// /home/user/requests-2.10.0/dist-info/METADATA
globParsers := map[string]common.ParserFn{ globParsers := map[string]common.ParserFn{
"**/*egg-info/PKG-INFO": parseEggMetadata, "**/*egg-info/PKG-INFO": parseEggMetadata,
"**/*dist-info/METADATA": parseWheelMetadata, "**/*dist-info/METADATA": parseWheelMetadata,
@ -27,14 +26,17 @@ func NewCataloger() *Cataloger {
} }
} }
// Name returns a string that uniquely describes this cataloger.
func (a *Cataloger) Name() string { func (a *Cataloger) Name() string {
return "python-cataloger" return "python-cataloger"
} }
// SelectFiles returns a set of discovered Python ecosystem files from the user content source.
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference { func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
return a.cataloger.SelectFiles(resolver) return a.cataloger.SelectFiles(resolver)
} }
// Catalog returns the Packages indexed from all Python ecosystem files discovered.
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) { func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
return a.cataloger.Catalog(contents, a.Name()) return a.cataloger.Catalog(contents, a.Name())
} }

View File

@ -4,10 +4,15 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/pelletier/go-toml" "github.com/pelletier/go-toml"
) )
// integrity check
var _ common.ParserFn = parsePoetryLock
// parsePoetryLock is a parser function for poetry.lock contents, returning all python packages discovered.
func parsePoetryLock(_ string, reader io.Reader) ([]pkg.Package, error) { func parsePoetryLock(_ string, reader io.Reader) ([]pkg.Package, error) {
tree, err := toml.LoadReader(reader) tree, err := toml.LoadReader(reader)
if err != nil { if err != nil {

View File

@ -6,9 +6,15 @@ import (
"io" "io"
"strings" "strings"
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
) )
// integrity check
var _ common.ParserFn = parseRequirementsTxt
// parseRequirementsTxt takes a Python requirements.txt file, returning all Python packages that are locked to a
// specific version.
func parseRequirementsTxt(_ string, reader io.Reader) ([]pkg.Package, error) { func parseRequirementsTxt(_ string, reader io.Reader) ([]pkg.Package, error) {
packages := make([]pkg.Package, 0) packages := make([]pkg.Package, 0)
@ -55,6 +61,7 @@ func parseRequirementsTxt(_ string, reader io.Reader) ([]pkg.Package, error) {
return packages, nil return packages, nil
} }
// removeTrailingComment takes a requirements.txt line and strips off comment strings.
func removeTrailingComment(line string) string { func removeTrailingComment(line string) string {
parts := strings.Split(line, "#") parts := strings.Split(line, "#")
switch len(parts) { switch len(parts) {

View File

@ -6,9 +6,16 @@ import (
"io" "io"
"strings" "strings"
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
) )
// integrity check
var _ common.ParserFn = parseWheelMetadata
var _ common.ParserFn = parseEggMetadata
// parseWheelMetadata is a parser function for individual Python Wheel metadata file contents, returning all Python
// packages listed.
func parseWheelMetadata(_ string, reader io.Reader) ([]pkg.Package, error) { func parseWheelMetadata(_ string, reader io.Reader) ([]pkg.Package, error) {
packages, err := parseWheelOrEggMetadata(reader) packages, err := parseWheelOrEggMetadata(reader)
for idx := range packages { for idx := range packages {
@ -17,6 +24,8 @@ func parseWheelMetadata(_ string, reader io.Reader) ([]pkg.Package, error) {
return packages, err return packages, err
} }
// parseEggMetadata is a parser function for individual Python Egg metadata file contents, returning all Python
// packages listed.
func parseEggMetadata(_ string, reader io.Reader) ([]pkg.Package, error) { func parseEggMetadata(_ string, reader io.Reader) ([]pkg.Package, error) {
packages, err := parseWheelOrEggMetadata(reader) packages, err := parseWheelOrEggMetadata(reader)
for idx := range packages { for idx := range packages {
@ -25,6 +34,8 @@ func parseEggMetadata(_ string, reader io.Reader) ([]pkg.Package, error) {
return packages, err return packages, err
} }
// parseWheelOrEggMetadata takes a Python Egg or Wheel (which share the same format and values for our purposes),
// returning all Python packages listed.
func parseWheelOrEggMetadata(reader io.Reader) ([]pkg.Package, error) { func parseWheelOrEggMetadata(reader io.Reader) ([]pkg.Package, error) {
fields := make(map[string]string) fields := make(map[string]string)
var key string var key string

View File

@ -7,11 +7,13 @@ import (
"github.com/anchore/syft/syft/scope" "github.com/anchore/syft/syft/scope"
) )
// Cataloger catalogs pkg.RpmPkg Package Types defined in RPM DB files.
type Cataloger struct { type Cataloger struct {
cataloger common.GenericCataloger cataloger common.GenericCataloger
} }
func NewCataloger() *Cataloger { // New returns a new RPM DB cataloger object.
func New() *Cataloger {
globParsers := map[string]common.ParserFn{ globParsers := map[string]common.ParserFn{
"**/var/lib/rpm/Packages": parseRpmDB, "**/var/lib/rpm/Packages": parseRpmDB,
} }
@ -21,14 +23,17 @@ func NewCataloger() *Cataloger {
} }
} }
// Name returns a string that uniquely describes this cataloger.
func (a *Cataloger) Name() string { func (a *Cataloger) Name() string {
return "rpmdb-cataloger" return "rpmdb-cataloger"
} }
// SelectFiles returns a set of discovered RPM DB files from the user content source.
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference { func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
return a.cataloger.SelectFiles(resolver) return a.cataloger.SelectFiles(resolver)
} }
// Catalog returns the Packages indexed from all RPM DB files discovered.
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) { func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
return a.cataloger.Catalog(contents, a.Name()) return a.cataloger.Catalog(contents, a.Name())
} }

View File

@ -9,9 +9,14 @@ import (
rpmdb "github.com/anchore/go-rpmdb/pkg" rpmdb "github.com/anchore/go-rpmdb/pkg"
"github.com/anchore/syft/internal" "github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
) )
// integrity check
var _ common.ParserFn = parseRpmDB
// parseApkDb parses an "Packages" RPM DB and returns the Packages listed within it.
func parseRpmDB(_ string, reader io.Reader) ([]pkg.Package, error) { func parseRpmDB(_ string, reader io.Reader) ([]pkg.Package, error) {
f, err := ioutil.TempFile("", internal.ApplicationName+"-rpmdb") f, err := ioutil.TempFile("", internal.ApplicationName+"-rpmdb")
if err != nil { if err != nil {

View File

@ -41,7 +41,7 @@ func IdentifyDistro(s scope.Scope) distro.Distro {
func CatalogFromScope(s scope.Scope) (*pkg.Catalog, error) { func CatalogFromScope(s scope.Scope) (*pkg.Catalog, error) {
log.Info("building the catalog") log.Info("building the catalog")
return cataloger.Catalog(s) return cataloger.Catalog(s, cataloger.All()...)
} }
func SetLogger(logger logger.Logger) { func SetLogger(logger logger.Logger) {

View File

@ -1,7 +1,7 @@
package pkg package pkg
// Available fields are described at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html // DpkgMetadata represents all captured data for a Debian package DB entry; available fields are described
// in the --showformat section // at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html in the --showformat section.
type DpkgMetadata struct { type DpkgMetadata struct {
Package string `mapstructure:"Package" json:"package"` Package string `mapstructure:"Package" json:"package"`
Source string `mapstructure:"Source" json:"source"` Source string `mapstructure:"Source" json:"source"`
@ -9,6 +9,7 @@ type DpkgMetadata struct {
// TODO: consider keeping the remaining values as an embedded map // TODO: consider keeping the remaining values as an embedded map
} }
// RpmMetadata represents all captured data for a RHEL package DB entry.
type RpmMetadata struct { type RpmMetadata struct {
Version string `mapstructure:"Version" json:"version"` Version string `mapstructure:"Version" json:"version"`
Epoch int `mapstructure:"Epoch" json:"epoch"` Epoch int `mapstructure:"Epoch" json:"epoch"`

View File

@ -36,6 +36,8 @@ var (
dockerPullStageChars = strings.Split("▁▃▄▅▆▇█", "") dockerPullStageChars = strings.Split("▁▃▄▅▆▇█", "")
) )
// startProcess is a helper function for providing common elements for long-running UI elements (such as a
// progress bar formatter and status spinner)
func startProcess() (format.Simple, *common.Spinner) { func startProcess() (format.Simple, *common.Spinner) {
width, _ := frame.GetTerminalSize() width, _ := frame.GetTerminalSize()
barWidth := int(0.25 * float64(width)) barWidth := int(0.25 * float64(width))
@ -48,6 +50,7 @@ func startProcess() (format.Simple, *common.Spinner) {
return formatter, &spinner return formatter, &spinner
} }
// formatDockerPullPhase returns a single character that represents the status of a layer pull.
func formatDockerPullPhase(phase docker.PullPhase, inputStr string) string { func formatDockerPullPhase(phase docker.PullPhase, inputStr string) string {
switch phase { switch phase {
case docker.WaitingPhase: case docker.WaitingPhase:
@ -69,6 +72,7 @@ func formatDockerPullPhase(phase docker.PullPhase, inputStr string) string {
} }
// nolint:funlen // nolint:funlen
// formatDockerImagePullStatus writes the docker image pull status summarized into a single line for the given state.
func formatDockerImagePullStatus(pullStatus *docker.PullStatus, spinner *common.Spinner, line *frame.Line) { func formatDockerImagePullStatus(pullStatus *docker.PullStatus, spinner *common.Spinner, line *frame.Line) {
var size, current uint64 var size, current uint64
@ -136,6 +140,7 @@ func formatDockerImagePullStatus(pullStatus *docker.PullStatus, spinner *common.
_, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s%s", spin, title, progStr, auxInfo)) _, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s%s", spin, title, progStr, auxInfo))
} }
// PullDockerImageHandler periodically writes a formatted line widget representing a docker image pull event.
func PullDockerImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error { func PullDockerImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
_, pullStatus, err := stereoEventParsers.ParsePullDockerImage(event) _, pullStatus, err := stereoEventParsers.ParsePullDockerImage(event)
if err != nil { if err != nil {
@ -175,6 +180,7 @@ func PullDockerImageHandler(ctx context.Context, fr *frame.Frame, event partybus
return err return err
} }
// FetchImageHandler periodically writes a the image save and write-to-disk process in the form of a progress bar.
func FetchImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error { func FetchImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
_, prog, err := stereoEventParsers.ParseFetchImage(event) _, prog, err := stereoEventParsers.ParseFetchImage(event)
if err != nil { if err != nil {
@ -217,6 +223,7 @@ func FetchImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Even
return err return err
} }
// ReadImageHandler periodically writes a the image read/parse/build-tree status in the form of a progress bar.
func ReadImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error { func ReadImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
_, prog, err := stereoEventParsers.ParseReadImage(event) _, prog, err := stereoEventParsers.ParseReadImage(event)
if err != nil { if err != nil {
@ -260,6 +267,7 @@ func ReadImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event
return nil return nil
} }
// CatalogerStartedHandler periodically writes catalog statistics to a single line.
func CatalogerStartedHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error { func CatalogerStartedHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
monitor, err := syftEventParsers.ParseCatalogerStarted(event) monitor, err := syftEventParsers.ParseCatalogerStarted(event)
if err != nil { if err != nil {

View File

@ -1,3 +1,8 @@
/*
Package ui provides all public UI elements intended to be repurposed in other applications. Specifically, a single
Handler object is provided to allow consuming applications (such as grype) to check if there are UI elements the handler
can respond to (given a specific event type) and handle the event in context of the given screen frame object.
*/
package ui package ui
import ( import (
@ -10,13 +15,16 @@ import (
"github.com/wagoodman/jotframe/pkg/frame" "github.com/wagoodman/jotframe/pkg/frame"
) )
// Handler is an aggregated event handler for the set of supported events (PullDockerImage, ReadImage, FetchImage, CatalogerStarted)
type Handler struct { type Handler struct {
} }
// NewHandler returns an empty Handler
func NewHandler() *Handler { func NewHandler() *Handler {
return &Handler{} return &Handler{}
} }
// RespondsTo indicates if the handler is capable of handling the given event.
func (r *Handler) RespondsTo(event partybus.Event) bool { func (r *Handler) RespondsTo(event partybus.Event) bool {
switch event.Type { switch event.Type {
case stereoscopeEvent.PullDockerImage, stereoscopeEvent.ReadImage, stereoscopeEvent.FetchImage, syftEvent.CatalogerStarted: case stereoscopeEvent.PullDockerImage, stereoscopeEvent.ReadImage, stereoscopeEvent.FetchImage, syftEvent.CatalogerStarted:
@ -26,6 +34,7 @@ func (r *Handler) RespondsTo(event partybus.Event) bool {
} }
} }
// Handle calls the specific event handler for the given event within the context of the screen frame.
func (r *Handler) Handle(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error { func (r *Handler) Handle(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
switch event.Type { switch event.Type {
case stereoscopeEvent.PullDockerImage: case stereoscopeEvent.PullDockerImage: