Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch UI to bubbletea #1888

Merged
merged 9 commits into from
Jul 6, 2023
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
2 changes: 1 addition & 1 deletion .github/actions/bootstrap/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ inputs:
go-version:
description: "Go version to install"
required: true
default: "1.19.x"
default: "1.20.x"
use-go-cache:
description: "Restore go cache"
required: true
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
go.work
go.work.sum
/bin
/.bin
CHANGELOG.md
VERSION
Expand Down
1 change: 1 addition & 0 deletions cmd/syft/cli/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func Attest(v *viper.Viper, app *config.Application, ro *options.RootOptions, po
RunE: func(cmd *cobra.Command, args []string) error {
if app.CheckForAppUpdate {
checkForApplicationUpdate()
// TODO: this is broke, the bus isn't available yet
}

return attest.Run(cmd.Context(), app, args)
Expand Down
15 changes: 11 additions & 4 deletions cmd/syft/cli/attest/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ import (
"github.com/anchore/syft/cmd/syft/cli/eventloop"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/cli/packages"
"github.com/anchore/syft/cmd/syft/internal/ui"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/file"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/event/monitor"
Expand All @@ -39,6 +39,13 @@ func Run(_ context.Context, app *config.Application, args []string) error {
// note: must be a container image
userInput := args[0]

_, err = exec.LookPath("cosign")
if err != nil {
// when cosign is not installed the error will be rendered like so:
// 2023/06/30 08:31:52 error during command execution: 'syft attest' requires cosign to be installed: exec: "cosign": executable file not found in $PATH
return fmt.Errorf("'syft attest' requires cosign to be installed: %w", err)
}

eventBus := partybus.NewBus()
stereoscope.SetBus(eventBus)
syft.SetBus(eventBus)
Expand Down Expand Up @@ -119,7 +126,7 @@ func execWorker(app *config.Application, userInput string) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
defer bus.Publish(partybus.Event{Type: event.Exit})
defer bus.Exit()

s, err := buildSBOM(app, userInput, errs)
if err != nil {
Expand Down Expand Up @@ -207,8 +214,8 @@ func execWorker(app *config.Application, userInput string) <-chan error {
Context: "cosign",
},
Value: &monitor.ShellProgress{
Reader: r,
Manual: mon,
Reader: r,
Progressable: mon,
},
},
)
Expand Down
2 changes: 1 addition & 1 deletion cmd/syft/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func checkForApplicationUpdate() {
log.Infof("new version of %s is available: %s (current version is %s)", internal.ApplicationName, newVersion, version.FromBuild().Version)

bus.Publish(partybus.Event{
Type: event.AppUpdateAvailable,
Type: event.CLIAppUpdateAvailable,
Value: newVersion,
})
} else {
Expand Down
1 change: 1 addition & 0 deletions cmd/syft/cli/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func Convert(v *viper.Viper, app *config.Application, ro *options.RootOptions, p
RunE: func(cmd *cobra.Command, args []string) error {
if app.CheckForAppUpdate {
checkForApplicationUpdate()
// TODO: this is broke, the bus isn't available yet
}
return convert.Run(cmd.Context(), app, args)
},
Expand Down
51 changes: 45 additions & 6 deletions cmd/syft/cli/convert/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,29 @@ import (
"io"
"os"

"github.com/wagoodman/go-partybus"

"github.com/anchore/stereoscope"
"github.com/anchore/syft/cmd/syft/cli/eventloop"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/internal/ui"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/formats"
"github.com/anchore/syft/syft/sbom"
)

func Run(_ context.Context, app *config.Application, args []string) error {
log.Warn("convert is an experimental feature, run `syft convert -h` for help")

writer, err := options.MakeSBOMWriter(app.Outputs, app.File, app.OutputTemplatePath)
if err != nil {
return err
}

// this can only be a SBOM file
// could be an image or a directory, with or without a scheme
userInput := args[0]

var reader io.ReadCloser
Expand All @@ -37,10 +46,40 @@ func Run(_ context.Context, app *config.Application, args []string) error {
reader = f
}

sbom, _, err := formats.Decode(reader)
if err != nil {
return fmt.Errorf("failed to decode SBOM: %w", err)
}
eventBus := partybus.NewBus()
stereoscope.SetBus(eventBus)
syft.SetBus(eventBus)
subscription := eventBus.Subscribe()

return eventloop.EventLoop(
execWorker(reader, writer),
eventloop.SetupSignals(),
subscription,
stereoscope.Cleanup,
ui.Select(options.IsVerbose(app), app.Quiet)...,
)
}

func execWorker(reader io.Reader, writer sbom.Writer) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
defer bus.Exit()

return writer.Write(*sbom)
s, _, err := formats.Decode(reader)
if err != nil {
errs <- fmt.Errorf("failed to decode SBOM: %w", err)
return
}

if s == nil {
errs <- fmt.Errorf("no SBOM produced")
return
}

if err := writer.Write(*s); err != nil {
errs <- fmt.Errorf("failed to write SBOM: %w", err)
}
}()
return errs
}
14 changes: 7 additions & 7 deletions cmd/syft/cli/eventloop/event_loop.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@ import (
"github.com/hashicorp/go-multierror"
"github.com/wagoodman/go-partybus"

"github.com/anchore/clio"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
)

// eventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and
// EventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and
// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until
// an eventual graceful exit.
func EventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...ui.UI) error {
func EventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...clio.UI) error {
defer cleanupFn()
events := subscription.Events()
var err error
var ux ui.UI
var ux clio.UI

if ux, err = setupUI(subscription.Unsubscribe, uxs...); err != nil {
if ux, err = setupUI(subscription, uxs...); err != nil {
return err
}

Expand Down Expand Up @@ -85,9 +85,9 @@ func EventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *
// 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) {
func setupUI(subscription *partybus.Subscription, uis ...clio.UI) (clio.UI, error) {
for _, ux := range uis {
if err := ux.Setup(unsubscribe); err != nil {
if err := ux.Setup(subscription); err != nil {
log.Warnf("unable to setup given UI, falling back to alternative UI: %+v", err)
continue
}
Expand Down
29 changes: 16 additions & 13 deletions cmd/syft/cli/eventloop/event_loop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,37 @@ import (
"github.com/stretchr/testify/mock"
"github.com/wagoodman/go-partybus"

"github.com/anchore/syft/internal/ui"
"github.com/anchore/clio"
"github.com/anchore/syft/syft/event"
)

var _ ui.UI = (*uiMock)(nil)
var _ clio.UI = (*uiMock)(nil)

type uiMock struct {
t *testing.T
finalEvent partybus.Event
unsubscribe func() error
t *testing.T
finalEvent partybus.Event
subscription partybus.Unsubscribable
mock.Mock
}

func (u *uiMock) Setup(unsubscribe func() error) error {
func (u *uiMock) Setup(unsubscribe partybus.Unsubscribable) error {
u.t.Helper()
u.t.Logf("UI Setup called")
u.unsubscribe = unsubscribe
return u.Called(unsubscribe).Error(0)
u.subscription = unsubscribe
return u.Called(unsubscribe.Unsubscribe).Error(0)
}

func (u *uiMock) Handle(event partybus.Event) error {
u.t.Helper()
u.t.Logf("UI Handle called: %+v", event.Type)
if event == u.finalEvent {
assert.NoError(u.t, u.unsubscribe())
assert.NoError(u.t, u.subscription.Unsubscribe())
}
return u.Called(event).Error(0)
}

func (u *uiMock) Teardown(_ bool) error {
u.t.Helper()
u.t.Logf("UI Teardown called")
return u.Called().Error(0)
}
Expand All @@ -51,7 +54,7 @@ func Test_EventLoop_gracefulExit(t *testing.T) {
t.Cleanup(testBus.Close)

finalEvent := partybus.Event{
Type: event.Exit,
Type: event.CLIExit,
}

worker := func() <-chan error {
Expand Down Expand Up @@ -183,7 +186,7 @@ func Test_EventLoop_unsubscribeError(t *testing.T) {
t.Cleanup(testBus.Close)

finalEvent := partybus.Event{
Type: event.Exit,
Type: event.CLIExit,
}

worker := func() <-chan error {
Expand Down Expand Up @@ -252,7 +255,7 @@ func Test_EventLoop_handlerError(t *testing.T) {
t.Cleanup(testBus.Close)

finalEvent := partybus.Event{
Type: event.Exit,
Type: event.CLIExit,
Error: fmt.Errorf("an exit error occured"),
}

Expand Down Expand Up @@ -377,7 +380,7 @@ func Test_EventLoop_uiTeardownError(t *testing.T) {
t.Cleanup(testBus.Close)

finalEvent := partybus.Event{
Type: event.Exit,
Type: event.CLIExit,
}

worker := func() <-chan error {
Expand Down
29 changes: 19 additions & 10 deletions cmd/syft/cli/options/writer.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package options

import (
"bytes"
"fmt"
"io"
"os"
Expand All @@ -10,6 +11,7 @@ import (
"github.com/hashicorp/go-multierror"
"github.com/mitchellh/go-homedir"

"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/formats"
"github.com/anchore/syft/syft/formats/table"
Expand Down Expand Up @@ -114,14 +116,6 @@ type sbomMultiWriter struct {
writers []sbom.Writer
}

type nopWriteCloser struct {
io.Writer
}

func (n nopWriteCloser) Close() error {
return nil
}

// newSBOMMultiWriter create all report writers from input options; if a file is not specified the given defaultWriter is used
func newSBOMMultiWriter(options ...sbomWriterDescription) (_ *sbomMultiWriter, err error) {
if len(options) == 0 {
Expand All @@ -133,9 +127,8 @@ func newSBOMMultiWriter(options ...sbomWriterDescription) (_ *sbomMultiWriter, e
for _, option := range options {
switch len(option.Path) {
case 0:
out.writers = append(out.writers, &sbomStreamWriter{
out.writers = append(out.writers, &sbomPublisher{
format: option.Format,
out: nopWriteCloser{Writer: os.Stdout},
})
default:
// create any missing subdirectories
Expand Down Expand Up @@ -195,3 +188,19 @@ func (w *sbomStreamWriter) Close() error {
}
return nil
}

// sbomPublisher implements sbom.Writer that publishes results to the event bus
type sbomPublisher struct {
format sbom.Format
}

// Write the provided SBOM to the data stream
func (w *sbomPublisher) Write(s sbom.SBOM) error {
buf := &bytes.Buffer{}
if err := w.format.Encode(buf, s); err != nil {
return fmt.Errorf("unable to encode SBOM: %w", err)
}

bus.Report(buf.String())
return nil
}
2 changes: 2 additions & 0 deletions cmd/syft/cli/options/writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ func Test_newSBOMMultiWriter(t *testing.T) {
if e.file != "" {
assert.FileExists(t, tmp+e.file)
}
case *sbomPublisher:
assert.Equal(t, string(w.format.ID()), e.format)
default:
t.Fatalf("unknown writer type: %T", w)
}
Expand Down
1 change: 1 addition & 0 deletions cmd/syft/cli/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func Packages(v *viper.Viper, app *config.Application, ro *options.RootOptions,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
if app.CheckForAppUpdate {
// TODO: this is broke, the bus isn't available yet
checkForApplicationUpdate()
}
return packages.Run(cmd.Context(), app, args)
Expand Down
Loading