mirror of
https://github.com/anchore/syft.git
synced 2026-10-30 14:02:26 +01:00
Add option to output SBOM report to a file (#530)
* add output to file option Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * log errors on close of the report destination Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * remove file option from persistent args Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * update file option comments and logging Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * allow for multiple UI fallback options Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * update UI select signatures + tests Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
+16
-10
@@ -2,6 +2,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
@@ -14,11 +15,13 @@ import (
|
||||
// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until
|
||||
// an eventual graceful exit.
|
||||
// nolint:gocognit,funlen
|
||||
func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, ux ui.UI, cleanupFn func()) error {
|
||||
func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...ui.UI) error {
|
||||
defer cleanupFn()
|
||||
events := subscription.Events()
|
||||
var err error
|
||||
if ux, err = setupUI(subscription.Unsubscribe, ux); err != nil {
|
||||
var ux ui.UI
|
||||
|
||||
if ux, err = setupUI(subscription.Unsubscribe, uxs...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -78,15 +81,18 @@ func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *
|
||||
return retErr
|
||||
}
|
||||
|
||||
func setupUI(unsubscribe func() error, ux ui.UI) (ui.UI, error) {
|
||||
if err := ux.Setup(unsubscribe); err != nil {
|
||||
// replace the existing UI with a (simpler) logger UI
|
||||
ux = ui.NewLoggerUI()
|
||||
// setupUI takes one or more UIs that responds to events and takes a event bus unsubscribe function for use
|
||||
// during teardown. With the given UIs, the first UI which the ui.Setup() function does not return an error
|
||||
// will be utilized in execution. Providing a set of UIs allows for the caller to provide graceful fallbacks
|
||||
// when there are environmental problem (e.g. unable to setup a TUI with the current TTY).
|
||||
func setupUI(unsubscribe func() error, uis ...ui.UI) (ui.UI, error) {
|
||||
for _, ux := range uis {
|
||||
if err := ux.Setup(unsubscribe); err != nil {
|
||||
// something is very wrong, bail.
|
||||
return ux, err
|
||||
log.Warnf("unable to setup given UI, falling back to alternative UI: %+v", err)
|
||||
continue
|
||||
}
|
||||
log.Errorf("unable to setup given UI, falling back to logger: %+v", err)
|
||||
|
||||
return ux, nil
|
||||
}
|
||||
return ux, nil
|
||||
return nil, fmt.Errorf("unable to setup any UI")
|
||||
}
|
||||
|
||||
@@ -96,8 +96,8 @@ func Test_eventLoop_gracefulExit(t *testing.T) {
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
ux,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -159,8 +159,8 @@ func Test_eventLoop_workerError(t *testing.T) {
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
ux,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
workerErr,
|
||||
"should have seen a worker error, but did not",
|
||||
@@ -230,8 +230,8 @@ func Test_eventLoop_unsubscribeError(t *testing.T) {
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
ux,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -300,8 +300,8 @@ func Test_eventLoop_handlerError(t *testing.T) {
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
ux,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
finalEvent.Error,
|
||||
"should have seen a event error, but did not",
|
||||
@@ -355,8 +355,8 @@ func Test_eventLoop_signalsStopExecution(t *testing.T) {
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
ux,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -425,8 +425,8 @@ func Test_eventLoop_uiTeardownError(t *testing.T) {
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
ux,
|
||||
cleanupFn,
|
||||
ux,
|
||||
),
|
||||
teardownError,
|
||||
"should have seen a UI teardown error, but did not",
|
||||
|
||||
+22
-1
@@ -113,6 +113,11 @@ func setPackageFlags(flags *pflag.FlagSet) {
|
||||
fmt.Sprintf("report output formatter, options=%v", packages.AllPresenters),
|
||||
)
|
||||
|
||||
flags.StringP(
|
||||
"file", "", "",
|
||||
"file to write the report output to (default is STDOUT)",
|
||||
)
|
||||
|
||||
///////// Upload options //////////////////////////////////////////////////////////
|
||||
flags.StringP(
|
||||
"host", "H", "",
|
||||
@@ -156,6 +161,10 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := viper.BindPFlag("file", flags.Lookup("file")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
///////// Upload options //////////////////////////////////////////////////////////
|
||||
|
||||
if err := viper.BindPFlag("anchore.host", flags.Lookup("host")); err != nil {
|
||||
@@ -188,12 +197,24 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
|
||||
func packagesExec(_ *cobra.Command, args []string) error {
|
||||
// could be an image or a directory, with or without a scheme
|
||||
userInput := args[0]
|
||||
|
||||
reporter, closer, err := reportWriter()
|
||||
defer func() {
|
||||
if err := closer(); err != nil {
|
||||
log.Warnf("unable to write to report destination: %+v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return eventLoop(
|
||||
packagesExecWorker(userInput),
|
||||
setupSignals(),
|
||||
eventSubscription,
|
||||
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet),
|
||||
stereoscope.Cleanup,
|
||||
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet, reporter)...,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+14
-1
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/presenter/poweruser"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
@@ -73,12 +74,24 @@ func init() {
|
||||
func powerUserExec(_ *cobra.Command, args []string) error {
|
||||
// could be an image or a directory, with or without a scheme
|
||||
userInput := args[0]
|
||||
|
||||
reporter, closer, err := reportWriter()
|
||||
defer func() {
|
||||
if err := closer(); err != nil {
|
||||
log.Warnf("unable to write to report destination: %+v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return eventLoop(
|
||||
powerUserExecWorker(userInput),
|
||||
setupSignals(),
|
||||
eventSubscription,
|
||||
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet),
|
||||
stereoscope.Cleanup,
|
||||
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet, reporter)...,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
func reportWriter() (io.Writer, func() error, error) {
|
||||
nop := func() error { return nil }
|
||||
|
||||
path := strings.TrimSpace(appConfig.File)
|
||||
switch len(path) {
|
||||
case 0:
|
||||
return os.Stdout, nop, nil
|
||||
default:
|
||||
reportFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return nil, nop, fmt.Errorf("unable to create report file: %w", err)
|
||||
}
|
||||
return reportFile, func() error {
|
||||
log.Infof("report written to file=%q", path)
|
||||
return reportFile.Close()
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user