Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,9 @@ scope: "squashed"
# same as -q ; suppress all output (except for the vulnerability list)
quiet: false

# same as --file; write output report to a file (default is to write to stdout)
file: ""

db:
# check for database updates on execution
auto-update: true
Expand Down
26 changes: 16 additions & 10 deletions cmd/event_loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"errors"
"fmt"
"os"

"github.com/anchore/grype/internal/log"
Expand All @@ -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
}

Expand Down Expand Up @@ -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")
}
12 changes: 6 additions & 6 deletions cmd/event_loop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ func Test_eventLoop_gracefulExit(t *testing.T) {
worker(),
signaler(),
subscription,
ux,
cleanupFn,
ux,
),
)

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -230,8 +230,8 @@ func Test_eventLoop_unsubscribeError(t *testing.T) {
worker(),
signaler(),
subscription,
ux,
cleanupFn,
ux,
),
)

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -355,8 +355,8 @@ func Test_eventLoop_signalsStopExecution(t *testing.T) {
worker(),
signaler(),
subscription,
ux,
cleanupFn,
ux,
),
)

Expand Down Expand Up @@ -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",
Expand Down
29 changes: 29 additions & 0 deletions cmd/report_writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package cmd

import (
"fmt"
"io"
"os"
"strings"
)

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 {
if !appConfig.Quiet {
fmt.Printf("Report written to %q\n", path)
}
return reportFile.Close()
}, nil
}
Comment on lines +13 to +28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very small nit: This is a good example of code that's well structured, but could be more readable if vertical spacing were leveraged more. Blank lines to separate subsections of thoughts can be really nice for readers looking to get a sense of structure, particular with regard to control flow — e.g., using blank lines to make return lines more clear.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add some space to break this up 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
22 changes: 21 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ func setRootFlags(flags *pflag.FlagSet) {
fmt.Sprintf("report output formatter, formats=%v", presenter.AvailableFormats),
)

flags.StringP(
"file", "", "",
"file to write the report output to (default is STDOUT)",
)

flags.StringP("template", "t", "", "specify the path to a Go template file ("+
"requires 'template' output to be selected)")

Expand All @@ -120,6 +125,10 @@ func bindRootConfigOptions(flags *pflag.FlagSet) error {
return err
}

if err := viper.BindPFlag("file", flags.Lookup("file")); err != nil {
return err
}

if err := viper.BindPFlag("output-template-file", flags.Lookup("template")); err != nil {
return err
}
Expand All @@ -137,12 +146,23 @@ func rootExec(_ *cobra.Command, args []string) error {
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(
startWorker(userInput, appConfig.FailOnSeverity),
setupSignals(),
eventSubscription,
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet),
stereoscope.Cleanup,
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet, reporter)...,
)
}

Expand Down
1 change: 1 addition & 0 deletions internal/config/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type parser interface {
type Application struct {
ConfigPath string `yaml:",omitempty" json:"configPath"` // the location where the application config was read from (either from -c or discovered while loading)
Output string `yaml:"output" json:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
OutputTemplateFile string `yaml:"output-template-file" json:"output-template-file" mapstructure:"output-template-file"` // -t, the template file to use for formatting the final report
Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI)
CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
Expand Down
25 changes: 14 additions & 11 deletions internal/ui/ephemeral_terminal_ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,22 @@ import (
// 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)
type ephemeralTerminalUI struct {
unsubscribe func() error
handler *ui.Handler
waitGroup *sync.WaitGroup
frame *frame.Frame
logBuffer *bytes.Buffer
uiOutput *os.File
unsubscribe func() error
handler *ui.Handler
waitGroup *sync.WaitGroup
frame *frame.Frame
logBuffer *bytes.Buffer
uiOutput *os.File
reportOutput io.Writer
}

func NewEphemeralTerminalUI() UI {
// NewEphemeralTerminalUI writes all events to a TUI and writes the final report to the given writer.
func NewEphemeralTerminalUI(reportWriter io.Writer) UI {
return &ephemeralTerminalUI{
handler: ui.NewHandler(),
waitGroup: &sync.WaitGroup{},
uiOutput: os.Stderr,
handler: ui.NewHandler(),
waitGroup: &sync.WaitGroup{},
uiOutput: os.Stderr,
reportOutput: reportWriter,
}
}

Expand Down Expand Up @@ -81,7 +84,7 @@ func (h *ephemeralTerminalUI) Handle(event partybus.Event) error {
// are about to write bytes to stdout, so we should reset the terminal state first
h.closeScreen(false)

if err := handleVulnerabilityScanningFinished(event); err != nil {
if err := handleVulnerabilityScanningFinished(event, h.reportOutput); err != nil {
log.Errorf("unable to show %s event: %+v", event.Type, err)
}

Expand Down
5 changes: 2 additions & 3 deletions internal/ui/event_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"fmt"
"io"
"os"
"sync"

grypeEventParsers "github.com/anchore/grype/grype/event/parsers"
Expand All @@ -14,14 +13,14 @@ import (
"github.com/wagoodman/jotframe/pkg/frame"
)

func handleVulnerabilityScanningFinished(event partybus.Event) error {
func handleVulnerabilityScanningFinished(event partybus.Event, reportOutput io.Writer) error {
// show the report to stdout
pres, err := grypeEventParsers.ParseVulnerabilityScanningFinished(event)
if err != nil {
return fmt.Errorf("bad CatalogerFinished event: %w", err)
}

if err := pres.Present(os.Stdout); err != nil {
if err := pres.Present(reportOutput); err != nil {
return fmt.Errorf("unable to show vulnerability report: %w", err)
}
return nil
Expand Down
14 changes: 10 additions & 4 deletions internal/ui/logger_ui.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
package ui

import (
"io"

grypeEvent "github.com/anchore/grype/grype/event"
"github.com/anchore/grype/internal/log"
"github.com/wagoodman/go-partybus"
)

type loggerUI struct {
unsubscribe func() error
unsubscribe func() error
reportOutput io.Writer
}

func NewLoggerUI() UI {
return &loggerUI{}
// NewLoggerUI writes all events to the common application logger and writes the final report to the given writer.
func NewLoggerUI(reportWriter io.Writer) UI {
return &loggerUI{
reportOutput: reportWriter,
}
}

func (l *loggerUI) Setup(unsubscribe func() error) error {
Expand All @@ -25,7 +31,7 @@ func (l loggerUI) Handle(event partybus.Event) error {
return nil
}

if err := handleVulnerabilityScanningFinished(event); err != nil {
if err := handleVulnerabilityScanningFinished(event, l.reportOutput); err != nil {
log.Warnf("unable to show catalog image finished event: %+v", err)
}

Expand Down
16 changes: 9 additions & 7 deletions internal/ui/select.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ui

import (
"io"
"os"
"runtime"

Expand All @@ -10,20 +11,21 @@ import (
// 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 {
var ui UI

// 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
// are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of
// the final SBOM report.
func Select(verbose, quiet bool, reportWriter io.Writer) (uis []UI) {
isStdoutATty := terminal.IsTerminal(int(os.Stdout.Fd()))
isStderrATty := terminal.IsTerminal(int(os.Stderr.Fd()))
notATerminal := !isStderrATty && !isStdoutATty

switch {
case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty:
ui = NewLoggerUI()
uis = append(uis, NewLoggerUI(reportWriter))
default:
ui = NewEphemeralTerminalUI()
uis = append(uis, NewEphemeralTerminalUI(reportWriter), NewLoggerUI(reportWriter))
}

return ui
return uis
}