mirror of
https://github.com/anchore/syft
synced 2024-11-10 06:14:16 +00:00
Switch UI to bubbletea (#1888)
* add bubbletea UI Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * swap pipeline to go 1.20.x and add attest guard for cosign binary Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * update note in developing.md about the required golang version Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * fix merge conflict for windows path handling Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * temp test for attest handler Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add addtional test iterations for background reader Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <alex.goodman@anchore.com> Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
parent
a00a3df10c
commit
f8b832e6c3
77 changed files with 3250 additions and 618 deletions
2
.github/actions/bootstrap/action.yaml
vendored
2
.github/actions/bootstrap/action.yaml
vendored
|
@ -4,7 +4,7 @@ inputs:
|
||||||
go-version:
|
go-version:
|
||||||
description: "Go version to install"
|
description: "Go version to install"
|
||||||
required: true
|
required: true
|
||||||
default: "1.19.x"
|
default: "1.20.x"
|
||||||
use-go-cache:
|
use-go-cache:
|
||||||
description: "Restore go cache"
|
description: "Restore go cache"
|
||||||
required: true
|
required: true
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
||||||
go.work
|
go.work
|
||||||
go.work.sum
|
go.work.sum
|
||||||
|
/bin
|
||||||
/.bin
|
/.bin
|
||||||
CHANGELOG.md
|
CHANGELOG.md
|
||||||
VERSION
|
VERSION
|
||||||
|
|
|
@ -43,6 +43,7 @@ func Attest(v *viper.Viper, app *config.Application, ro *options.RootOptions, po
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if app.CheckForAppUpdate {
|
if app.CheckForAppUpdate {
|
||||||
checkForApplicationUpdate()
|
checkForApplicationUpdate()
|
||||||
|
// TODO: this is broke, the bus isn't available yet
|
||||||
}
|
}
|
||||||
|
|
||||||
return attest.Run(cmd.Context(), app, args)
|
return attest.Run(cmd.Context(), app, args)
|
||||||
|
|
|
@ -16,11 +16,11 @@ import (
|
||||||
"github.com/anchore/syft/cmd/syft/cli/eventloop"
|
"github.com/anchore/syft/cmd/syft/cli/eventloop"
|
||||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||||
"github.com/anchore/syft/cmd/syft/cli/packages"
|
"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/bus"
|
||||||
"github.com/anchore/syft/internal/config"
|
"github.com/anchore/syft/internal/config"
|
||||||
"github.com/anchore/syft/internal/file"
|
"github.com/anchore/syft/internal/file"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/internal/ui"
|
|
||||||
"github.com/anchore/syft/syft"
|
"github.com/anchore/syft/syft"
|
||||||
"github.com/anchore/syft/syft/event"
|
"github.com/anchore/syft/syft/event"
|
||||||
"github.com/anchore/syft/syft/event/monitor"
|
"github.com/anchore/syft/syft/event/monitor"
|
||||||
|
@ -39,6 +39,13 @@ func Run(_ context.Context, app *config.Application, args []string) error {
|
||||||
// note: must be a container image
|
// note: must be a container image
|
||||||
userInput := args[0]
|
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()
|
eventBus := partybus.NewBus()
|
||||||
stereoscope.SetBus(eventBus)
|
stereoscope.SetBus(eventBus)
|
||||||
syft.SetBus(eventBus)
|
syft.SetBus(eventBus)
|
||||||
|
@ -119,7 +126,7 @@ func execWorker(app *config.Application, userInput string) <-chan error {
|
||||||
errs := make(chan error)
|
errs := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
defer close(errs)
|
defer close(errs)
|
||||||
defer bus.Publish(partybus.Event{Type: event.Exit})
|
defer bus.Exit()
|
||||||
|
|
||||||
s, err := buildSBOM(app, userInput, errs)
|
s, err := buildSBOM(app, userInput, errs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -208,7 +215,7 @@ func execWorker(app *config.Application, userInput string) <-chan error {
|
||||||
},
|
},
|
||||||
Value: &monitor.ShellProgress{
|
Value: &monitor.ShellProgress{
|
||||||
Reader: r,
|
Reader: r,
|
||||||
Manual: mon,
|
Progressable: mon,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
log.Infof("new version of %s is available: %s (current version is %s)", internal.ApplicationName, newVersion, version.FromBuild().Version)
|
||||||
|
|
||||||
bus.Publish(partybus.Event{
|
bus.Publish(partybus.Event{
|
||||||
Type: event.AppUpdateAvailable,
|
Type: event.CLIAppUpdateAvailable,
|
||||||
Value: newVersion,
|
Value: newVersion,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -43,6 +43,7 @@ func Convert(v *viper.Viper, app *config.Application, ro *options.RootOptions, p
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if app.CheckForAppUpdate {
|
if app.CheckForAppUpdate {
|
||||||
checkForApplicationUpdate()
|
checkForApplicationUpdate()
|
||||||
|
// TODO: this is broke, the bus isn't available yet
|
||||||
}
|
}
|
||||||
return convert.Run(cmd.Context(), app, args)
|
return convert.Run(cmd.Context(), app, args)
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,20 +6,29 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"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/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/config"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft"
|
||||||
"github.com/anchore/syft/syft/formats"
|
"github.com/anchore/syft/syft/formats"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Run(_ context.Context, app *config.Application, args []string) error {
|
func Run(_ context.Context, app *config.Application, args []string) error {
|
||||||
log.Warn("convert is an experimental feature, run `syft convert -h` for help")
|
log.Warn("convert is an experimental feature, run `syft convert -h` for help")
|
||||||
|
|
||||||
writer, err := options.MakeSBOMWriter(app.Outputs, app.File, app.OutputTemplatePath)
|
writer, err := options.MakeSBOMWriter(app.Outputs, app.File, app.OutputTemplatePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// this can only be a SBOM file
|
// could be an image or a directory, with or without a scheme
|
||||||
userInput := args[0]
|
userInput := args[0]
|
||||||
|
|
||||||
var reader io.ReadCloser
|
var reader io.ReadCloser
|
||||||
|
@ -37,10 +46,40 @@ func Run(_ context.Context, app *config.Application, args []string) error {
|
||||||
reader = f
|
reader = f
|
||||||
}
|
}
|
||||||
|
|
||||||
sbom, _, err := formats.Decode(reader)
|
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()
|
||||||
|
|
||||||
|
s, _, err := formats.Decode(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to decode SBOM: %w", err)
|
errs <- fmt.Errorf("failed to decode SBOM: %w", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return writer.Write(*sbom)
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,20 +8,20 @@ import (
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
"github.com/wagoodman/go-partybus"
|
"github.com/wagoodman/go-partybus"
|
||||||
|
|
||||||
|
"github.com/anchore/clio"
|
||||||
"github.com/anchore/syft/internal/log"
|
"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
|
// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until
|
||||||
// an eventual graceful exit.
|
// 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()
|
defer cleanupFn()
|
||||||
events := subscription.Events()
|
events := subscription.Events()
|
||||||
var err error
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
// 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
|
// 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).
|
// 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 {
|
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)
|
log.Warnf("unable to setup given UI, falling back to alternative UI: %+v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,34 +11,37 @@ import (
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/wagoodman/go-partybus"
|
"github.com/wagoodman/go-partybus"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/ui"
|
"github.com/anchore/clio"
|
||||||
"github.com/anchore/syft/syft/event"
|
"github.com/anchore/syft/syft/event"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ ui.UI = (*uiMock)(nil)
|
var _ clio.UI = (*uiMock)(nil)
|
||||||
|
|
||||||
type uiMock struct {
|
type uiMock struct {
|
||||||
t *testing.T
|
t *testing.T
|
||||||
finalEvent partybus.Event
|
finalEvent partybus.Event
|
||||||
unsubscribe func() error
|
subscription partybus.Unsubscribable
|
||||||
mock.Mock
|
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.t.Logf("UI Setup called")
|
||||||
u.unsubscribe = unsubscribe
|
u.subscription = unsubscribe
|
||||||
return u.Called(unsubscribe).Error(0)
|
return u.Called(unsubscribe.Unsubscribe).Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *uiMock) Handle(event partybus.Event) error {
|
func (u *uiMock) Handle(event partybus.Event) error {
|
||||||
|
u.t.Helper()
|
||||||
u.t.Logf("UI Handle called: %+v", event.Type)
|
u.t.Logf("UI Handle called: %+v", event.Type)
|
||||||
if event == u.finalEvent {
|
if event == u.finalEvent {
|
||||||
assert.NoError(u.t, u.unsubscribe())
|
assert.NoError(u.t, u.subscription.Unsubscribe())
|
||||||
}
|
}
|
||||||
return u.Called(event).Error(0)
|
return u.Called(event).Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *uiMock) Teardown(_ bool) error {
|
func (u *uiMock) Teardown(_ bool) error {
|
||||||
|
u.t.Helper()
|
||||||
u.t.Logf("UI Teardown called")
|
u.t.Logf("UI Teardown called")
|
||||||
return u.Called().Error(0)
|
return u.Called().Error(0)
|
||||||
}
|
}
|
||||||
|
@ -51,7 +54,7 @@ func Test_EventLoop_gracefulExit(t *testing.T) {
|
||||||
t.Cleanup(testBus.Close)
|
t.Cleanup(testBus.Close)
|
||||||
|
|
||||||
finalEvent := partybus.Event{
|
finalEvent := partybus.Event{
|
||||||
Type: event.Exit,
|
Type: event.CLIExit,
|
||||||
}
|
}
|
||||||
|
|
||||||
worker := func() <-chan error {
|
worker := func() <-chan error {
|
||||||
|
@ -183,7 +186,7 @@ func Test_EventLoop_unsubscribeError(t *testing.T) {
|
||||||
t.Cleanup(testBus.Close)
|
t.Cleanup(testBus.Close)
|
||||||
|
|
||||||
finalEvent := partybus.Event{
|
finalEvent := partybus.Event{
|
||||||
Type: event.Exit,
|
Type: event.CLIExit,
|
||||||
}
|
}
|
||||||
|
|
||||||
worker := func() <-chan error {
|
worker := func() <-chan error {
|
||||||
|
@ -252,7 +255,7 @@ func Test_EventLoop_handlerError(t *testing.T) {
|
||||||
t.Cleanup(testBus.Close)
|
t.Cleanup(testBus.Close)
|
||||||
|
|
||||||
finalEvent := partybus.Event{
|
finalEvent := partybus.Event{
|
||||||
Type: event.Exit,
|
Type: event.CLIExit,
|
||||||
Error: fmt.Errorf("an exit error occured"),
|
Error: fmt.Errorf("an exit error occured"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -377,7 +380,7 @@ func Test_EventLoop_uiTeardownError(t *testing.T) {
|
||||||
t.Cleanup(testBus.Close)
|
t.Cleanup(testBus.Close)
|
||||||
|
|
||||||
finalEvent := partybus.Event{
|
finalEvent := partybus.Event{
|
||||||
Type: event.Exit,
|
Type: event.CLIExit,
|
||||||
}
|
}
|
||||||
|
|
||||||
worker := func() <-chan error {
|
worker := func() <-chan error {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package options
|
package options
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
@ -10,6 +11,7 @@ import (
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
"github.com/mitchellh/go-homedir"
|
"github.com/mitchellh/go-homedir"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/bus"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/syft/formats"
|
"github.com/anchore/syft/syft/formats"
|
||||||
"github.com/anchore/syft/syft/formats/table"
|
"github.com/anchore/syft/syft/formats/table"
|
||||||
|
@ -114,14 +116,6 @@ type sbomMultiWriter struct {
|
||||||
writers []sbom.Writer
|
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
|
// 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) {
|
func newSBOMMultiWriter(options ...sbomWriterDescription) (_ *sbomMultiWriter, err error) {
|
||||||
if len(options) == 0 {
|
if len(options) == 0 {
|
||||||
|
@ -133,9 +127,8 @@ func newSBOMMultiWriter(options ...sbomWriterDescription) (_ *sbomMultiWriter, e
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
switch len(option.Path) {
|
switch len(option.Path) {
|
||||||
case 0:
|
case 0:
|
||||||
out.writers = append(out.writers, &sbomStreamWriter{
|
out.writers = append(out.writers, &sbomPublisher{
|
||||||
format: option.Format,
|
format: option.Format,
|
||||||
out: nopWriteCloser{Writer: os.Stdout},
|
|
||||||
})
|
})
|
||||||
default:
|
default:
|
||||||
// create any missing subdirectories
|
// create any missing subdirectories
|
||||||
|
@ -195,3 +188,19 @@ func (w *sbomStreamWriter) Close() error {
|
||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -191,6 +191,8 @@ func Test_newSBOMMultiWriter(t *testing.T) {
|
||||||
if e.file != "" {
|
if e.file != "" {
|
||||||
assert.FileExists(t, tmp+e.file)
|
assert.FileExists(t, tmp+e.file)
|
||||||
}
|
}
|
||||||
|
case *sbomPublisher:
|
||||||
|
assert.Equal(t, string(w.format.ID()), e.format)
|
||||||
default:
|
default:
|
||||||
t.Fatalf("unknown writer type: %T", w)
|
t.Fatalf("unknown writer type: %T", w)
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,7 @@ func Packages(v *viper.Viper, app *config.Application, ro *options.RootOptions,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if app.CheckForAppUpdate {
|
if app.CheckForAppUpdate {
|
||||||
|
// TODO: this is broke, the bus isn't available yet
|
||||||
checkForApplicationUpdate()
|
checkForApplicationUpdate()
|
||||||
}
|
}
|
||||||
return packages.Run(cmd.Context(), app, args)
|
return packages.Run(cmd.Context(), app, args)
|
||||||
|
|
|
@ -10,15 +10,14 @@ import (
|
||||||
"github.com/anchore/stereoscope/pkg/image"
|
"github.com/anchore/stereoscope/pkg/image"
|
||||||
"github.com/anchore/syft/cmd/syft/cli/eventloop"
|
"github.com/anchore/syft/cmd/syft/cli/eventloop"
|
||||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||||
|
"github.com/anchore/syft/cmd/syft/internal/ui"
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/internal/bus"
|
"github.com/anchore/syft/internal/bus"
|
||||||
"github.com/anchore/syft/internal/config"
|
"github.com/anchore/syft/internal/config"
|
||||||
"github.com/anchore/syft/internal/file"
|
"github.com/anchore/syft/internal/file"
|
||||||
"github.com/anchore/syft/internal/ui"
|
|
||||||
"github.com/anchore/syft/internal/version"
|
"github.com/anchore/syft/internal/version"
|
||||||
"github.com/anchore/syft/syft"
|
"github.com/anchore/syft/syft"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/anchore/syft/syft/event"
|
|
||||||
"github.com/anchore/syft/syft/formats/template"
|
"github.com/anchore/syft/syft/formats/template"
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
|
@ -52,10 +51,12 @@ func Run(_ context.Context, app *config.Application, args []string) error {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nolint:funlen
|
||||||
func execWorker(app *config.Application, userInput string, writer sbom.Writer) <-chan error {
|
func execWorker(app *config.Application, userInput string, writer sbom.Writer) <-chan error {
|
||||||
errs := make(chan error)
|
errs := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
defer close(errs)
|
defer close(errs)
|
||||||
|
defer bus.Exit()
|
||||||
|
|
||||||
detection, err := source.Detect(
|
detection, err := source.Detect(
|
||||||
userInput,
|
userInput,
|
||||||
|
@ -115,12 +116,13 @@ func execWorker(app *config.Application, userInput string, writer sbom.Writer) <
|
||||||
|
|
||||||
if s == nil {
|
if s == nil {
|
||||||
errs <- fmt.Errorf("no SBOM produced for %q", userInput)
|
errs <- fmt.Errorf("no SBOM produced for %q", userInput)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bus.Publish(partybus.Event{
|
if err := writer.Write(*s); err != nil {
|
||||||
Type: event.Exit,
|
errs <- fmt.Errorf("failed to write SBOM: %w", err)
|
||||||
Value: func() error { return writer.Write(*s) },
|
return
|
||||||
})
|
}
|
||||||
}()
|
}()
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,7 @@ func PowerUser(v *viper.Viper, app *config.Application, ro *options.RootOptions)
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if app.CheckForAppUpdate {
|
if app.CheckForAppUpdate {
|
||||||
checkForApplicationUpdate()
|
checkForApplicationUpdate()
|
||||||
|
// TODO: this is broke, the bus isn't available yet
|
||||||
}
|
}
|
||||||
return poweruser.Run(cmd.Context(), app, args)
|
return poweruser.Run(cmd.Context(), app, args)
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,14 +13,13 @@ import (
|
||||||
"github.com/anchore/syft/cmd/syft/cli/eventloop"
|
"github.com/anchore/syft/cmd/syft/cli/eventloop"
|
||||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||||
"github.com/anchore/syft/cmd/syft/cli/packages"
|
"github.com/anchore/syft/cmd/syft/cli/packages"
|
||||||
|
"github.com/anchore/syft/cmd/syft/internal/ui"
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/internal/bus"
|
"github.com/anchore/syft/internal/bus"
|
||||||
"github.com/anchore/syft/internal/config"
|
"github.com/anchore/syft/internal/config"
|
||||||
"github.com/anchore/syft/internal/ui"
|
|
||||||
"github.com/anchore/syft/internal/version"
|
"github.com/anchore/syft/internal/version"
|
||||||
"github.com/anchore/syft/syft"
|
"github.com/anchore/syft/syft"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/anchore/syft/syft/event"
|
|
||||||
"github.com/anchore/syft/syft/formats/syftjson"
|
"github.com/anchore/syft/syft/formats/syftjson"
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
|
@ -59,6 +58,7 @@ func execWorker(app *config.Application, userInput string, writer sbom.Writer) <
|
||||||
errs := make(chan error)
|
errs := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
defer close(errs)
|
defer close(errs)
|
||||||
|
defer bus.Exit()
|
||||||
|
|
||||||
app.Secrets.Cataloger.Enabled = true
|
app.Secrets.Cataloger.Enabled = true
|
||||||
app.FileMetadata.Cataloger.Enabled = true
|
app.FileMetadata.Cataloger.Enabled = true
|
||||||
|
@ -133,10 +133,10 @@ func execWorker(app *config.Application, userInput string, writer sbom.Writer) <
|
||||||
|
|
||||||
s.Relationships = append(s.Relationships, packages.MergeRelationships(relationships...)...)
|
s.Relationships = append(s.Relationships, packages.MergeRelationships(relationships...)...)
|
||||||
|
|
||||||
bus.Publish(partybus.Event{
|
if err := writer.Write(s); err != nil {
|
||||||
Type: event.Exit,
|
errs <- fmt.Errorf("failed to write sbom: %w", err)
|
||||||
Value: func() error { return writer.Write(s) },
|
return
|
||||||
})
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return errs
|
return errs
|
||||||
|
|
19
cmd/syft/cli/ui/__snapshots__/handle_attestation_test.snap
Executable file
19
cmd/syft/cli/ui/__snapshots__/handle_attestation_test.snap
Executable file
|
@ -0,0 +1,19 @@
|
||||||
|
|
||||||
|
[TestHandler_handleAttestationStarted/attesting_in_progress/task_line - 1]
|
||||||
|
⠋ Creating a thing running a thing
|
||||||
|
---
|
||||||
|
|
||||||
|
[TestHandler_handleAttestationStarted/attesting_in_progress/log - 1]
|
||||||
|
░░ contents
|
||||||
|
░░ of
|
||||||
|
░░ stuff!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[TestHandler_handleAttestationStarted/attesting_complete/task_line - 1]
|
||||||
|
✔ Created a thing running a thing
|
||||||
|
---
|
||||||
|
|
||||||
|
[TestHandler_handleAttestationStarted/attesting_complete/log - 1]
|
||||||
|
|
||||||
|
---
|
16
cmd/syft/cli/ui/__snapshots__/handle_cataloger_task_test.snap
Executable file
16
cmd/syft/cli/ui/__snapshots__/handle_cataloger_task_test.snap
Executable file
|
@ -0,0 +1,16 @@
|
||||||
|
|
||||||
|
[TestHandler_handleCatalogerTaskStarted/cataloging_task_in_progress - 1]
|
||||||
|
some task title [some value]
|
||||||
|
---
|
||||||
|
|
||||||
|
[TestHandler_handleCatalogerTaskStarted/cataloging_sub_task_in_progress - 1]
|
||||||
|
└── some task title [some value]
|
||||||
|
---
|
||||||
|
|
||||||
|
[TestHandler_handleCatalogerTaskStarted/cataloging_sub_task_complete - 1]
|
||||||
|
✔ └── some task done [some value]
|
||||||
|
---
|
||||||
|
|
||||||
|
[TestHandler_handleCatalogerTaskStarted/cataloging_sub_task_complete_with_removal - 1]
|
||||||
|
|
||||||
|
---
|
8
cmd/syft/cli/ui/__snapshots__/handle_fetch_image_test.snap
Executable file
8
cmd/syft/cli/ui/__snapshots__/handle_fetch_image_test.snap
Executable file
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
[TestHandler_handleFetchImage/fetch_image_in_progress - 1]
|
||||||
|
⠋ Loading image ━━━━━━━━━━━━━━━━━━━━ [current] the-image
|
||||||
|
---
|
||||||
|
|
||||||
|
[TestHandler_handleFetchImage/fetch_image_complete - 1]
|
||||||
|
✔ Loaded image the-image
|
||||||
|
---
|
8
cmd/syft/cli/ui/__snapshots__/handle_file_digests_cataloger_test.snap
Executable file
8
cmd/syft/cli/ui/__snapshots__/handle_file_digests_cataloger_test.snap
Executable file
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
[TestHandler_handleFileDigestsCatalogerStarted/cataloging_in_progress - 1]
|
||||||
|
⠋ Cataloging file digests ━━━━━━━━━━━━━━━━━━━━ [current]
|
||||||
|
---
|
||||||
|
|
||||||
|
[TestHandler_handleFileDigestsCatalogerStarted/cataloging_complete - 1]
|
||||||
|
✔ Cataloged file digests
|
||||||
|
---
|
8
cmd/syft/cli/ui/__snapshots__/handle_file_indexing_test.snap
Executable file
8
cmd/syft/cli/ui/__snapshots__/handle_file_indexing_test.snap
Executable file
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
[TestHandler_handleFileIndexingStarted/cataloging_in_progress - 1]
|
||||||
|
⠋ Indexing file system ━━━━━━━━━━━━━━━━━━━━ [current] /some/path
|
||||||
|
---
|
||||||
|
|
||||||
|
[TestHandler_handleFileIndexingStarted/cataloging_complete - 1]
|
||||||
|
✔ Indexed file system /some/path
|
||||||
|
---
|
8
cmd/syft/cli/ui/__snapshots__/handle_file_metadata_cataloger_test.snap
Executable file
8
cmd/syft/cli/ui/__snapshots__/handle_file_metadata_cataloger_test.snap
Executable file
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
[TestHandler_handleFileMetadataCatalogerStarted/cataloging_in_progress - 1]
|
||||||
|
⠋ Cataloging file metadata ━━━━━━━━━━━━━━━━━━━━ [current]
|
||||||
|
---
|
||||||
|
|
||||||
|
[TestHandler_handleFileMetadataCatalogerStarted/cataloging_complete - 1]
|
||||||
|
✔ Cataloged file metadata
|
||||||
|
---
|
16
cmd/syft/cli/ui/__snapshots__/handle_package_cataloger_test.snap
Executable file
16
cmd/syft/cli/ui/__snapshots__/handle_package_cataloger_test.snap
Executable file
|
@ -0,0 +1,16 @@
|
||||||
|
|
||||||
|
[TestHandler_handlePackageCatalogerStarted/cataloging_in_progress - 1]
|
||||||
|
⠋ Cataloging packages [50 packages]
|
||||||
|
---
|
||||||
|
|
||||||
|
[TestHandler_handlePackageCatalogerStarted/cataloging_only_files_complete - 1]
|
||||||
|
⠋ Cataloging packages [50 packages]
|
||||||
|
---
|
||||||
|
|
||||||
|
[TestHandler_handlePackageCatalogerStarted/cataloging_only_packages_complete - 1]
|
||||||
|
⠋ Cataloging packages [100 packages]
|
||||||
|
---
|
||||||
|
|
||||||
|
[TestHandler_handlePackageCatalogerStarted/cataloging_complete - 1]
|
||||||
|
✔ Cataloged packages [100 packages]
|
||||||
|
---
|
12
cmd/syft/cli/ui/__snapshots__/handle_pull_docker_image_test.snap
Executable file
12
cmd/syft/cli/ui/__snapshots__/handle_pull_docker_image_test.snap
Executable file
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
[Test_dockerPullStatusFormatter_Render/pulling - 1]
|
||||||
|
3 Layers▕▅▃ ▏[12 B / 30 B]
|
||||||
|
---
|
||||||
|
|
||||||
|
[Test_dockerPullStatusFormatter_Render/download_complete - 1]
|
||||||
|
3 Layers▕█▃ ▏[30 B] Extracting...
|
||||||
|
---
|
||||||
|
|
||||||
|
[Test_dockerPullStatusFormatter_Render/complete - 1]
|
||||||
|
3 Layers▕███▏[30 B] Extracting...
|
||||||
|
---
|
8
cmd/syft/cli/ui/__snapshots__/handle_read_image_test.snap
Executable file
8
cmd/syft/cli/ui/__snapshots__/handle_read_image_test.snap
Executable file
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
[TestHandler_handleReadImage/read_image_in_progress - 1]
|
||||||
|
⠋ Parsing image ━━━━━━━━━━━━━━━━━━━━ id
|
||||||
|
---
|
||||||
|
|
||||||
|
[TestHandler_handleReadImage/read_image_complete - 1]
|
||||||
|
✔ Parsed image id
|
||||||
|
---
|
8
cmd/syft/cli/ui/__snapshots__/handle_secrets_cataloger_test.snap
Executable file
8
cmd/syft/cli/ui/__snapshots__/handle_secrets_cataloger_test.snap
Executable file
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
[TestHandler_handleSecretsCatalogerStarted/cataloging_in_progress - 1]
|
||||||
|
⠋ Cataloging secrets ━━━━━━━━━━━━━━━━━━━━ [64 secrets]
|
||||||
|
---
|
||||||
|
|
||||||
|
[TestHandler_handleSecretsCatalogerStarted/cataloging_complete - 1]
|
||||||
|
✔ Cataloged secrets [64 secrets]
|
||||||
|
---
|
247
cmd/syft/cli/ui/handle_attestation.go
Normal file
247
cmd/syft/cli/ui/handle_attestation.go
Normal file
|
@ -0,0 +1,247 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
"github.com/wagoodman/go-progress"
|
||||||
|
"github.com/zyedidia/generic/queue"
|
||||||
|
|
||||||
|
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ tea.Model = (*attestLogFrame)(nil)
|
||||||
|
_ cosignOutputReader = (*backgroundLineReader)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
type attestLogFrame struct {
|
||||||
|
reader cosignOutputReader
|
||||||
|
prog progress.Progressable
|
||||||
|
lines []string
|
||||||
|
completed bool
|
||||||
|
failed bool
|
||||||
|
windowSize tea.WindowSizeMsg
|
||||||
|
|
||||||
|
id uint32
|
||||||
|
sequence int
|
||||||
|
|
||||||
|
updateDuration time.Duration
|
||||||
|
borderStype lipgloss.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// attestLogFrameTickMsg indicates that the timer has ticked and we should render a frame.
|
||||||
|
type attestLogFrameTickMsg struct {
|
||||||
|
Time time.Time
|
||||||
|
Sequence int
|
||||||
|
ID uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type cosignOutputReader interface {
|
||||||
|
Lines() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type backgroundLineReader struct {
|
||||||
|
limit int
|
||||||
|
lines *queue.Queue[string]
|
||||||
|
lock *sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Handler) handleAttestationStarted(e partybus.Event) []tea.Model {
|
||||||
|
reader, prog, taskInfo, err := syftEventParsers.ParseAttestationStartedEvent(e)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("error", err).Warn("unable to parse event")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
stage := progress.Stage{}
|
||||||
|
|
||||||
|
tsk := m.newTaskProgress(
|
||||||
|
taskprogress.Title{
|
||||||
|
Default: taskInfo.Title.Default,
|
||||||
|
Running: taskInfo.Title.WhileRunning,
|
||||||
|
Success: taskInfo.Title.OnSuccess,
|
||||||
|
},
|
||||||
|
taskprogress.WithStagedProgressable(
|
||||||
|
struct {
|
||||||
|
progress.Progressable
|
||||||
|
progress.Stager
|
||||||
|
}{
|
||||||
|
Progressable: prog,
|
||||||
|
Stager: &stage,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
tsk.HideStageOnSuccess = false
|
||||||
|
|
||||||
|
if taskInfo.Context != "" {
|
||||||
|
tsk.Context = []string{taskInfo.Context}
|
||||||
|
}
|
||||||
|
|
||||||
|
borderStyle := tsk.HintStyle
|
||||||
|
|
||||||
|
return []tea.Model{
|
||||||
|
tsk,
|
||||||
|
newLogFrame(newBackgroundLineReader(m.Running, reader, &stage), prog, borderStyle),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLogFrame(reader cosignOutputReader, prog progress.Progressable, borderStyle lipgloss.Style) attestLogFrame {
|
||||||
|
return attestLogFrame{
|
||||||
|
reader: reader,
|
||||||
|
prog: prog,
|
||||||
|
id: uuid.Must(uuid.NewUUID()).ID(),
|
||||||
|
updateDuration: 250 * time.Millisecond,
|
||||||
|
borderStype: borderStyle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBackgroundLineReader(wg *sync.WaitGroup, reader io.Reader, stage *progress.Stage) *backgroundLineReader {
|
||||||
|
wg.Add(1)
|
||||||
|
r := &backgroundLineReader{
|
||||||
|
limit: 7,
|
||||||
|
lock: &sync.RWMutex{},
|
||||||
|
lines: queue.New[string](),
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
r.read(reader, stage)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *backgroundLineReader) read(reader io.Reader, stage *progress.Stage) {
|
||||||
|
s := bufio.NewScanner(reader)
|
||||||
|
|
||||||
|
for s.Scan() {
|
||||||
|
l.lock.Lock()
|
||||||
|
|
||||||
|
text := s.Text()
|
||||||
|
l.lines.Enqueue(text)
|
||||||
|
|
||||||
|
if strings.Contains(text, "tlog entry created with index") {
|
||||||
|
fields := strings.SplitN(text, ":", 2)
|
||||||
|
present := text
|
||||||
|
if len(fields) == 2 {
|
||||||
|
present = fmt.Sprintf("transparency log index: %s", fields[1])
|
||||||
|
}
|
||||||
|
stage.Current = present
|
||||||
|
} else if strings.Contains(text, "WARNING: skipping transparency log upload") {
|
||||||
|
stage.Current = "transparency log upload skipped"
|
||||||
|
}
|
||||||
|
|
||||||
|
// only show the last X lines of the shell output
|
||||||
|
for l.lines.Len() > l.limit {
|
||||||
|
l.lines.Dequeue()
|
||||||
|
}
|
||||||
|
|
||||||
|
l.lock.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l backgroundLineReader) Lines() []string {
|
||||||
|
l.lock.RLock()
|
||||||
|
defer l.lock.RUnlock()
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
|
||||||
|
l.lines.Each(func(line string) {
|
||||||
|
lines = append(lines, line)
|
||||||
|
})
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l attestLogFrame) Init() tea.Cmd {
|
||||||
|
// this is the periodic update of state information
|
||||||
|
return func() tea.Msg {
|
||||||
|
return attestLogFrameTickMsg{
|
||||||
|
// The time at which the tick occurred.
|
||||||
|
Time: time.Now(),
|
||||||
|
|
||||||
|
// The ID of the log frame that this message belongs to. This can be
|
||||||
|
// helpful when routing messages, however bear in mind that log frames
|
||||||
|
// will ignore messages that don't contain ID by default.
|
||||||
|
ID: l.id,
|
||||||
|
|
||||||
|
Sequence: l.sequence,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l attestLogFrame) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
l.windowSize = msg
|
||||||
|
return l, nil
|
||||||
|
|
||||||
|
case attestLogFrameTickMsg:
|
||||||
|
l.lines = l.reader.Lines()
|
||||||
|
|
||||||
|
l.completed = progress.IsCompleted(l.prog)
|
||||||
|
err := l.prog.Error()
|
||||||
|
l.failed = err != nil && !progress.IsErrCompleted(err)
|
||||||
|
|
||||||
|
tickCmd := l.handleTick(msg)
|
||||||
|
|
||||||
|
return l, tickCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
return l, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l attestLogFrame) View() string {
|
||||||
|
if l.completed && !l.failed {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
sb := strings.Builder{}
|
||||||
|
|
||||||
|
for _, line := range l.lines {
|
||||||
|
sb.WriteString(fmt.Sprintf(" %s %s\n", l.borderStype.Render("░░"), line))
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l attestLogFrame) queueNextTick() tea.Cmd {
|
||||||
|
return tea.Tick(l.updateDuration, func(t time.Time) tea.Msg {
|
||||||
|
return attestLogFrameTickMsg{
|
||||||
|
Time: t,
|
||||||
|
ID: l.id,
|
||||||
|
Sequence: l.sequence,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *attestLogFrame) handleTick(msg attestLogFrameTickMsg) tea.Cmd {
|
||||||
|
// If an ID is set, and the ID doesn't belong to this log frame, reject the message.
|
||||||
|
if msg.ID > 0 && msg.ID != l.id {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a sequence is set, and it's not the one we expect, reject the message.
|
||||||
|
// This prevents the log frame from receiving too many messages and
|
||||||
|
// thus updating too frequently.
|
||||||
|
if msg.Sequence > 0 && msg.Sequence != l.sequence {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
l.sequence++
|
||||||
|
|
||||||
|
// note: even if the log is completed we should still respond to stage changes and window size events
|
||||||
|
return l.queueNextTick()
|
||||||
|
}
|
133
cmd/syft/cli/ui/handle_attestation_test.go
Normal file
133
cmd/syft/cli/ui/handle_attestation_test.go
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/gkampitakis/go-snaps/snaps"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
"github.com/wagoodman/go-progress"
|
||||||
|
|
||||||
|
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||||
|
syftEvent "github.com/anchore/syft/syft/event"
|
||||||
|
"github.com/anchore/syft/syft/event/monitor"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandler_handleAttestationStarted(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
eventFn func(*testing.T) partybus.Event
|
||||||
|
iterations int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "attesting in progress",
|
||||||
|
// note: this model depends on a background reader. Multiple iterations ensures that the
|
||||||
|
// reader has time to at least start and process the test fixture before the runModel
|
||||||
|
// test harness completes (which is a fake event loop anyway).
|
||||||
|
iterations: 2,
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
reader := strings.NewReader("contents\nof\nstuff!")
|
||||||
|
|
||||||
|
src := monitor.GenericTask{
|
||||||
|
Title: monitor.Title{
|
||||||
|
Default: "Create a thing",
|
||||||
|
WhileRunning: "Creating a thing",
|
||||||
|
OnSuccess: "Created a thing",
|
||||||
|
},
|
||||||
|
Context: "running a thing",
|
||||||
|
}
|
||||||
|
|
||||||
|
mon := progress.NewManual(-1)
|
||||||
|
mon.Set(50)
|
||||||
|
|
||||||
|
value := &monitor.ShellProgress{
|
||||||
|
Reader: reader,
|
||||||
|
Progressable: mon,
|
||||||
|
}
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: syftEvent.AttestationStarted,
|
||||||
|
Source: src,
|
||||||
|
Value: value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "attesting complete",
|
||||||
|
// note: this model depends on a background reader. Multiple iterations ensures that the
|
||||||
|
// reader has time to at least start and process the test fixture before the runModel
|
||||||
|
// test harness completes (which is a fake event loop anyway).
|
||||||
|
iterations: 2,
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
reader := strings.NewReader("contents\nof\nstuff!")
|
||||||
|
|
||||||
|
src := monitor.GenericTask{
|
||||||
|
Title: monitor.Title{
|
||||||
|
Default: "Create a thing",
|
||||||
|
WhileRunning: "Creating a thing",
|
||||||
|
OnSuccess: "Created a thing",
|
||||||
|
},
|
||||||
|
Context: "running a thing",
|
||||||
|
}
|
||||||
|
|
||||||
|
mon := progress.NewManual(-1)
|
||||||
|
mon.Set(50)
|
||||||
|
mon.SetCompleted()
|
||||||
|
|
||||||
|
value := &monitor.ShellProgress{
|
||||||
|
Reader: reader,
|
||||||
|
Progressable: mon,
|
||||||
|
}
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: syftEvent.AttestationStarted,
|
||||||
|
Source: src,
|
||||||
|
Value: value,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
event := tt.eventFn(t)
|
||||||
|
handler := New(DefaultHandlerConfig())
|
||||||
|
handler.WindowSize = tea.WindowSizeMsg{
|
||||||
|
Width: 100,
|
||||||
|
Height: 80,
|
||||||
|
}
|
||||||
|
|
||||||
|
models := handler.Handle(event)
|
||||||
|
require.Len(t, models, 2)
|
||||||
|
|
||||||
|
t.Run("task line", func(t *testing.T) {
|
||||||
|
tsk, ok := models[0].(taskprogress.Model)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||||
|
Time: time.Now(),
|
||||||
|
Sequence: tsk.Sequence(),
|
||||||
|
ID: tsk.ID(),
|
||||||
|
})
|
||||||
|
t.Log(got)
|
||||||
|
snaps.MatchSnapshot(t, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("log", func(t *testing.T) {
|
||||||
|
log, ok := models[1].(attestLogFrame)
|
||||||
|
require.True(t, ok)
|
||||||
|
got := runModel(t, log, tt.iterations, attestLogFrameTickMsg{
|
||||||
|
Time: time.Now(),
|
||||||
|
Sequence: log.sequence,
|
||||||
|
ID: log.id,
|
||||||
|
})
|
||||||
|
t.Log(got)
|
||||||
|
snaps.MatchSnapshot(t, got)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
72
cmd/syft/cli/ui/handle_cataloger_task.go
Normal file
72
cmd/syft/cli/ui/handle_cataloger_task.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
"github.com/wagoodman/go-progress"
|
||||||
|
|
||||||
|
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft/event/monitor"
|
||||||
|
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ progress.Stager = (*catalogerTaskStageAdapter)(nil)
|
||||||
|
|
||||||
|
type catalogerTaskStageAdapter struct {
|
||||||
|
mon *monitor.CatalogerTask
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCatalogerTaskStageAdapter(mon *monitor.CatalogerTask) *catalogerTaskStageAdapter {
|
||||||
|
return &catalogerTaskStageAdapter{
|
||||||
|
mon: mon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c catalogerTaskStageAdapter) Stage() string {
|
||||||
|
return c.mon.GetValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Handler) handleCatalogerTaskStarted(e partybus.Event) []tea.Model {
|
||||||
|
mon, err := syftEventParsers.ParseCatalogerTaskStarted(e)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("error", err).Warn("unable to parse event")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var prefix string
|
||||||
|
if mon.SubStatus {
|
||||||
|
// TODO: support list of sub-statuses, not just a single leaf
|
||||||
|
prefix = "└── "
|
||||||
|
}
|
||||||
|
|
||||||
|
tsk := m.newTaskProgress(
|
||||||
|
taskprogress.Title{
|
||||||
|
// TODO: prefix should not be part of the title, but instead a separate field that is aware of the tree structure
|
||||||
|
Default: prefix + mon.Title,
|
||||||
|
Running: prefix + mon.Title,
|
||||||
|
Success: prefix + mon.TitleOnCompletion,
|
||||||
|
},
|
||||||
|
taskprogress.WithStagedProgressable(
|
||||||
|
struct {
|
||||||
|
progress.Stager
|
||||||
|
progress.Progressable
|
||||||
|
}{
|
||||||
|
Progressable: mon.GetMonitor(),
|
||||||
|
Stager: newCatalogerTaskStageAdapter(mon),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: this isn't ideal since the model stays around after it is no longer needed, but it works for now
|
||||||
|
tsk.HideOnSuccess = mon.RemoveOnCompletion
|
||||||
|
tsk.HideStageOnSuccess = false
|
||||||
|
tsk.HideProgressOnSuccess = false
|
||||||
|
|
||||||
|
tsk.TitleStyle = lipgloss.NewStyle()
|
||||||
|
// TODO: this is a hack to get the spinner to not show up, but ideally the component would support making the spinner optional
|
||||||
|
tsk.Spinner.Spinner.Frames = []string{" "}
|
||||||
|
|
||||||
|
return []tea.Model{tsk}
|
||||||
|
}
|
123
cmd/syft/cli/ui/handle_cataloger_task_test.go
Normal file
123
cmd/syft/cli/ui/handle_cataloger_task_test.go
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/gkampitakis/go-snaps/snaps"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
|
||||||
|
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||||
|
syftEvent "github.com/anchore/syft/syft/event"
|
||||||
|
"github.com/anchore/syft/syft/event/monitor"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandler_handleCatalogerTaskStarted(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
eventFn func(*testing.T) partybus.Event
|
||||||
|
iterations int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "cataloging task in progress",
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
src := &monitor.CatalogerTask{
|
||||||
|
SubStatus: false,
|
||||||
|
RemoveOnCompletion: false,
|
||||||
|
Title: "some task title",
|
||||||
|
TitleOnCompletion: "some task done",
|
||||||
|
}
|
||||||
|
|
||||||
|
src.SetValue("some value")
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: syftEvent.CatalogerTaskStarted,
|
||||||
|
Source: src,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cataloging sub task in progress",
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
src := &monitor.CatalogerTask{
|
||||||
|
SubStatus: true,
|
||||||
|
RemoveOnCompletion: false,
|
||||||
|
Title: "some task title",
|
||||||
|
TitleOnCompletion: "some task done",
|
||||||
|
}
|
||||||
|
|
||||||
|
src.SetValue("some value")
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: syftEvent.CatalogerTaskStarted,
|
||||||
|
Source: src,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cataloging sub task complete",
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
src := &monitor.CatalogerTask{
|
||||||
|
SubStatus: true,
|
||||||
|
RemoveOnCompletion: false,
|
||||||
|
Title: "some task title",
|
||||||
|
TitleOnCompletion: "some task done",
|
||||||
|
}
|
||||||
|
|
||||||
|
src.SetValue("some value")
|
||||||
|
src.SetCompleted()
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: syftEvent.CatalogerTaskStarted,
|
||||||
|
Source: src,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cataloging sub task complete with removal",
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
src := &monitor.CatalogerTask{
|
||||||
|
SubStatus: true,
|
||||||
|
RemoveOnCompletion: true,
|
||||||
|
Title: "some task title",
|
||||||
|
TitleOnCompletion: "some task done",
|
||||||
|
}
|
||||||
|
|
||||||
|
src.SetValue("some value")
|
||||||
|
src.SetCompleted()
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: syftEvent.CatalogerTaskStarted,
|
||||||
|
Source: src,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
event := tt.eventFn(t)
|
||||||
|
handler := New(DefaultHandlerConfig())
|
||||||
|
handler.WindowSize = tea.WindowSizeMsg{
|
||||||
|
Width: 100,
|
||||||
|
Height: 80,
|
||||||
|
}
|
||||||
|
|
||||||
|
models := handler.Handle(event)
|
||||||
|
require.Len(t, models, 1)
|
||||||
|
model := models[0]
|
||||||
|
|
||||||
|
tsk, ok := model.(taskprogress.Model)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||||
|
Time: time.Now(),
|
||||||
|
Sequence: tsk.Sequence(),
|
||||||
|
ID: tsk.ID(),
|
||||||
|
})
|
||||||
|
t.Log(got)
|
||||||
|
snaps.MatchSnapshot(t, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
32
cmd/syft/cli/ui/handle_fetch_image.go
Normal file
32
cmd/syft/cli/ui/handle_fetch_image.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
|
||||||
|
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||||
|
stereoEventParsers "github.com/anchore/stereoscope/pkg/event/parsers"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Handler) handleFetchImage(e partybus.Event) []tea.Model {
|
||||||
|
imgName, prog, err := stereoEventParsers.ParseFetchImage(e)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("error", err).Warn("unable to parse event")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tsk := m.newTaskProgress(
|
||||||
|
taskprogress.Title{
|
||||||
|
Default: "Load image",
|
||||||
|
Running: "Loading image",
|
||||||
|
Success: "Loaded image",
|
||||||
|
},
|
||||||
|
taskprogress.WithStagedProgressable(prog),
|
||||||
|
)
|
||||||
|
if imgName != "" {
|
||||||
|
tsk.Context = []string{imgName}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []tea.Model{tsk}
|
||||||
|
}
|
99
cmd/syft/cli/ui/handle_fetch_image_test.go
Normal file
99
cmd/syft/cli/ui/handle_fetch_image_test.go
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/gkampitakis/go-snaps/snaps"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
"github.com/wagoodman/go-progress"
|
||||||
|
|
||||||
|
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||||
|
stereoscopeEvent "github.com/anchore/stereoscope/pkg/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandler_handleFetchImage(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
eventFn func(*testing.T) partybus.Event
|
||||||
|
iterations int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "fetch image in progress",
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
prog := &progress.Manual{}
|
||||||
|
prog.SetTotal(100)
|
||||||
|
prog.Set(50)
|
||||||
|
|
||||||
|
mon := struct {
|
||||||
|
progress.Progressable
|
||||||
|
progress.Stager
|
||||||
|
}{
|
||||||
|
Progressable: prog,
|
||||||
|
Stager: &progress.Stage{
|
||||||
|
Current: "current",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: stereoscopeEvent.FetchImage,
|
||||||
|
Source: "the-image",
|
||||||
|
Value: mon,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fetch image complete",
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
prog := &progress.Manual{}
|
||||||
|
prog.SetTotal(100)
|
||||||
|
prog.Set(100)
|
||||||
|
prog.SetCompleted()
|
||||||
|
|
||||||
|
mon := struct {
|
||||||
|
progress.Progressable
|
||||||
|
progress.Stager
|
||||||
|
}{
|
||||||
|
Progressable: prog,
|
||||||
|
Stager: &progress.Stage{
|
||||||
|
Current: "current",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: stereoscopeEvent.FetchImage,
|
||||||
|
Source: "the-image",
|
||||||
|
Value: mon,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
event := tt.eventFn(t)
|
||||||
|
handler := New(DefaultHandlerConfig())
|
||||||
|
handler.WindowSize = tea.WindowSizeMsg{
|
||||||
|
Width: 100,
|
||||||
|
Height: 80,
|
||||||
|
}
|
||||||
|
|
||||||
|
models := handler.Handle(event)
|
||||||
|
require.Len(t, models, 1)
|
||||||
|
model := models[0]
|
||||||
|
|
||||||
|
tsk, ok := model.(taskprogress.Model)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||||
|
Time: time.Now(),
|
||||||
|
Sequence: tsk.Sequence(),
|
||||||
|
ID: tsk.ID(),
|
||||||
|
})
|
||||||
|
t.Log(got)
|
||||||
|
snaps.MatchSnapshot(t, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
28
cmd/syft/cli/ui/handle_file_digests_cataloger.go
Normal file
28
cmd/syft/cli/ui/handle_file_digests_cataloger.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
|
||||||
|
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Handler) handleFileDigestsCatalogerStarted(e partybus.Event) []tea.Model {
|
||||||
|
prog, err := syftEventParsers.ParseFileDigestsCatalogingStarted(e)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("error", err).Warn("unable to parse event")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tsk := m.newTaskProgress(
|
||||||
|
taskprogress.Title{
|
||||||
|
Default: "Catalog file digests",
|
||||||
|
Running: "Cataloging file digests",
|
||||||
|
Success: "Cataloged file digests",
|
||||||
|
}, taskprogress.WithStagedProgressable(prog),
|
||||||
|
)
|
||||||
|
|
||||||
|
return []tea.Model{tsk}
|
||||||
|
}
|
97
cmd/syft/cli/ui/handle_file_digests_cataloger_test.go
Normal file
97
cmd/syft/cli/ui/handle_file_digests_cataloger_test.go
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/gkampitakis/go-snaps/snaps"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
"github.com/wagoodman/go-progress"
|
||||||
|
|
||||||
|
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||||
|
syftEvent "github.com/anchore/syft/syft/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandler_handleFileDigestsCatalogerStarted(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
eventFn func(*testing.T) partybus.Event
|
||||||
|
iterations int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "cataloging in progress",
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
prog := &progress.Manual{}
|
||||||
|
prog.SetTotal(100)
|
||||||
|
prog.Set(50)
|
||||||
|
|
||||||
|
mon := struct {
|
||||||
|
progress.Progressable
|
||||||
|
progress.Stager
|
||||||
|
}{
|
||||||
|
Progressable: prog,
|
||||||
|
Stager: &progress.Stage{
|
||||||
|
Current: "current",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: syftEvent.FileDigestsCatalogerStarted,
|
||||||
|
Value: mon,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cataloging complete",
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
prog := &progress.Manual{}
|
||||||
|
prog.SetTotal(100)
|
||||||
|
prog.Set(100)
|
||||||
|
prog.SetCompleted()
|
||||||
|
|
||||||
|
mon := struct {
|
||||||
|
progress.Progressable
|
||||||
|
progress.Stager
|
||||||
|
}{
|
||||||
|
Progressable: prog,
|
||||||
|
Stager: &progress.Stage{
|
||||||
|
Current: "current",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: syftEvent.FileDigestsCatalogerStarted,
|
||||||
|
Value: mon,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
event := tt.eventFn(t)
|
||||||
|
handler := New(DefaultHandlerConfig())
|
||||||
|
handler.WindowSize = tea.WindowSizeMsg{
|
||||||
|
Width: 100,
|
||||||
|
Height: 80,
|
||||||
|
}
|
||||||
|
|
||||||
|
models := handler.Handle(event)
|
||||||
|
require.Len(t, models, 1)
|
||||||
|
model := models[0]
|
||||||
|
|
||||||
|
tsk, ok := model.(taskprogress.Model)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||||
|
Time: time.Now(),
|
||||||
|
Sequence: tsk.Sequence(),
|
||||||
|
ID: tsk.ID(),
|
||||||
|
})
|
||||||
|
t.Log(got)
|
||||||
|
snaps.MatchSnapshot(t, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
31
cmd/syft/cli/ui/handle_file_indexing.go
Normal file
31
cmd/syft/cli/ui/handle_file_indexing.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
|
||||||
|
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Handler) handleFileIndexingStarted(e partybus.Event) []tea.Model {
|
||||||
|
path, prog, err := syftEventParsers.ParseFileIndexingStarted(e)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("error", err).Warn("unable to parse event")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tsk := m.newTaskProgress(
|
||||||
|
taskprogress.Title{
|
||||||
|
Default: "Index files system",
|
||||||
|
Running: "Indexing file system",
|
||||||
|
Success: "Indexed file system",
|
||||||
|
},
|
||||||
|
taskprogress.WithStagedProgressable(prog),
|
||||||
|
)
|
||||||
|
|
||||||
|
tsk.Context = []string{path}
|
||||||
|
|
||||||
|
return []tea.Model{tsk}
|
||||||
|
}
|
99
cmd/syft/cli/ui/handle_file_indexing_test.go
Normal file
99
cmd/syft/cli/ui/handle_file_indexing_test.go
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/gkampitakis/go-snaps/snaps"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
"github.com/wagoodman/go-progress"
|
||||||
|
|
||||||
|
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||||
|
syftEvent "github.com/anchore/syft/syft/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandler_handleFileIndexingStarted(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
eventFn func(*testing.T) partybus.Event
|
||||||
|
iterations int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "cataloging in progress",
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
prog := &progress.Manual{}
|
||||||
|
prog.SetTotal(100)
|
||||||
|
prog.Set(50)
|
||||||
|
|
||||||
|
mon := struct {
|
||||||
|
progress.Progressable
|
||||||
|
progress.Stager
|
||||||
|
}{
|
||||||
|
Progressable: prog,
|
||||||
|
Stager: &progress.Stage{
|
||||||
|
Current: "current",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: syftEvent.FileIndexingStarted,
|
||||||
|
Source: "/some/path",
|
||||||
|
Value: mon,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cataloging complete",
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
prog := &progress.Manual{}
|
||||||
|
prog.SetTotal(100)
|
||||||
|
prog.Set(100)
|
||||||
|
prog.SetCompleted()
|
||||||
|
|
||||||
|
mon := struct {
|
||||||
|
progress.Progressable
|
||||||
|
progress.Stager
|
||||||
|
}{
|
||||||
|
Progressable: prog,
|
||||||
|
Stager: &progress.Stage{
|
||||||
|
Current: "current",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: syftEvent.FileIndexingStarted,
|
||||||
|
Source: "/some/path",
|
||||||
|
Value: mon,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
event := tt.eventFn(t)
|
||||||
|
handler := New(DefaultHandlerConfig())
|
||||||
|
handler.WindowSize = tea.WindowSizeMsg{
|
||||||
|
Width: 100,
|
||||||
|
Height: 80,
|
||||||
|
}
|
||||||
|
|
||||||
|
models := handler.Handle(event)
|
||||||
|
require.Len(t, models, 1)
|
||||||
|
model := models[0]
|
||||||
|
|
||||||
|
tsk, ok := model.(taskprogress.Model)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||||
|
Time: time.Now(),
|
||||||
|
Sequence: tsk.Sequence(),
|
||||||
|
ID: tsk.ID(),
|
||||||
|
})
|
||||||
|
t.Log(got)
|
||||||
|
snaps.MatchSnapshot(t, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
29
cmd/syft/cli/ui/handle_file_metadata_cataloger.go
Normal file
29
cmd/syft/cli/ui/handle_file_metadata_cataloger.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
|
||||||
|
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Handler) handleFileMetadataCatalogerStarted(e partybus.Event) []tea.Model {
|
||||||
|
prog, err := syftEventParsers.ParseFileMetadataCatalogingStarted(e)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("error", err).Warn("unable to parse event")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tsk := m.newTaskProgress(
|
||||||
|
taskprogress.Title{
|
||||||
|
Default: "Catalog file metadata",
|
||||||
|
Running: "Cataloging file metadata",
|
||||||
|
Success: "Cataloged file metadata",
|
||||||
|
},
|
||||||
|
taskprogress.WithStagedProgressable(prog),
|
||||||
|
)
|
||||||
|
|
||||||
|
return []tea.Model{tsk}
|
||||||
|
}
|
97
cmd/syft/cli/ui/handle_file_metadata_cataloger_test.go
Normal file
97
cmd/syft/cli/ui/handle_file_metadata_cataloger_test.go
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/gkampitakis/go-snaps/snaps"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
"github.com/wagoodman/go-progress"
|
||||||
|
|
||||||
|
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||||
|
syftEvent "github.com/anchore/syft/syft/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandler_handleFileMetadataCatalogerStarted(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
eventFn func(*testing.T) partybus.Event
|
||||||
|
iterations int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "cataloging in progress",
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
prog := &progress.Manual{}
|
||||||
|
prog.SetTotal(100)
|
||||||
|
prog.Set(50)
|
||||||
|
|
||||||
|
mon := struct {
|
||||||
|
progress.Progressable
|
||||||
|
progress.Stager
|
||||||
|
}{
|
||||||
|
Progressable: prog,
|
||||||
|
Stager: &progress.Stage{
|
||||||
|
Current: "current",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: syftEvent.FileMetadataCatalogerStarted,
|
||||||
|
Value: mon,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cataloging complete",
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
prog := &progress.Manual{}
|
||||||
|
prog.SetTotal(100)
|
||||||
|
prog.Set(100)
|
||||||
|
prog.SetCompleted()
|
||||||
|
|
||||||
|
mon := struct {
|
||||||
|
progress.Progressable
|
||||||
|
progress.Stager
|
||||||
|
}{
|
||||||
|
Progressable: prog,
|
||||||
|
Stager: &progress.Stage{
|
||||||
|
Current: "current",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: syftEvent.FileMetadataCatalogerStarted,
|
||||||
|
Value: mon,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
event := tt.eventFn(t)
|
||||||
|
handler := New(DefaultHandlerConfig())
|
||||||
|
handler.WindowSize = tea.WindowSizeMsg{
|
||||||
|
Width: 100,
|
||||||
|
Height: 80,
|
||||||
|
}
|
||||||
|
|
||||||
|
models := handler.Handle(event)
|
||||||
|
require.Len(t, models, 1)
|
||||||
|
model := models[0]
|
||||||
|
|
||||||
|
tsk, ok := model.(taskprogress.Model)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||||
|
Time: time.Now(),
|
||||||
|
Sequence: tsk.Sequence(),
|
||||||
|
ID: tsk.ID(),
|
||||||
|
})
|
||||||
|
t.Log(got)
|
||||||
|
snaps.MatchSnapshot(t, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
87
cmd/syft/cli/ui/handle_package_cataloger.go
Normal file
87
cmd/syft/cli/ui/handle_package_cataloger.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
"github.com/wagoodman/go-progress"
|
||||||
|
|
||||||
|
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ progress.StagedProgressable = (*packageCatalogerProgressAdapter)(nil)
|
||||||
|
|
||||||
|
type packageCatalogerProgressAdapter struct {
|
||||||
|
monitor *cataloger.Monitor
|
||||||
|
monitors []progress.Monitorable
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPackageCatalogerProgressAdapter(monitor *cataloger.Monitor) packageCatalogerProgressAdapter {
|
||||||
|
return packageCatalogerProgressAdapter{
|
||||||
|
monitor: monitor,
|
||||||
|
monitors: []progress.Monitorable{
|
||||||
|
monitor.FilesProcessed,
|
||||||
|
monitor.PackagesDiscovered,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p packageCatalogerProgressAdapter) Stage() string {
|
||||||
|
return fmt.Sprintf("%d packages", p.monitor.PackagesDiscovered.Current())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p packageCatalogerProgressAdapter) Current() int64 {
|
||||||
|
return p.monitor.PackagesDiscovered.Current()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p packageCatalogerProgressAdapter) Error() error {
|
||||||
|
completedMonitors := 0
|
||||||
|
for _, monitor := range p.monitors {
|
||||||
|
err := monitor.Error()
|
||||||
|
if err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if progress.IsErrCompleted(err) {
|
||||||
|
completedMonitors++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// something went wrong
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if completedMonitors == len(p.monitors) && len(p.monitors) > 0 {
|
||||||
|
return p.monitors[0].Error()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p packageCatalogerProgressAdapter) Size() int64 {
|
||||||
|
// this is an inherently unknown value (indeterminate total number of packages to discover)
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Handler) handlePackageCatalogerStarted(e partybus.Event) []tea.Model {
|
||||||
|
monitor, err := syftEventParsers.ParsePackageCatalogerStarted(e)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("error", err).Warn("unable to parse event")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tsk := m.newTaskProgress(
|
||||||
|
taskprogress.Title{
|
||||||
|
Default: "Catalog packages",
|
||||||
|
Running: "Cataloging packages",
|
||||||
|
Success: "Cataloged packages",
|
||||||
|
},
|
||||||
|
taskprogress.WithStagedProgressable(
|
||||||
|
newPackageCatalogerProgressAdapter(monitor),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
tsk.HideStageOnSuccess = false
|
||||||
|
|
||||||
|
return []tea.Model{tsk}
|
||||||
|
}
|
133
cmd/syft/cli/ui/handle_package_cataloger_test.go
Normal file
133
cmd/syft/cli/ui/handle_package_cataloger_test.go
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/gkampitakis/go-snaps/snaps"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
"github.com/wagoodman/go-progress"
|
||||||
|
|
||||||
|
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||||
|
syftEvent "github.com/anchore/syft/syft/event"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandler_handlePackageCatalogerStarted(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
eventFn func(*testing.T) partybus.Event
|
||||||
|
iterations int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "cataloging in progress",
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
prog := &progress.Manual{}
|
||||||
|
prog.SetTotal(100)
|
||||||
|
prog.Set(50)
|
||||||
|
|
||||||
|
mon := cataloger.Monitor{
|
||||||
|
FilesProcessed: progress.NewManual(-1),
|
||||||
|
PackagesDiscovered: prog,
|
||||||
|
}
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: syftEvent.PackageCatalogerStarted,
|
||||||
|
Value: mon,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cataloging only files complete",
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
prog := &progress.Manual{}
|
||||||
|
prog.SetTotal(100)
|
||||||
|
prog.Set(50)
|
||||||
|
|
||||||
|
files := progress.NewManual(-1)
|
||||||
|
files.SetCompleted()
|
||||||
|
|
||||||
|
mon := cataloger.Monitor{
|
||||||
|
FilesProcessed: files,
|
||||||
|
PackagesDiscovered: prog,
|
||||||
|
}
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: syftEvent.PackageCatalogerStarted,
|
||||||
|
Value: mon,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cataloging only packages complete",
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
prog := &progress.Manual{}
|
||||||
|
prog.SetTotal(100)
|
||||||
|
prog.Set(100)
|
||||||
|
prog.SetCompleted()
|
||||||
|
|
||||||
|
files := progress.NewManual(-1)
|
||||||
|
|
||||||
|
mon := cataloger.Monitor{
|
||||||
|
FilesProcessed: files,
|
||||||
|
PackagesDiscovered: prog,
|
||||||
|
}
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: syftEvent.PackageCatalogerStarted,
|
||||||
|
Value: mon,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cataloging complete",
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
prog := &progress.Manual{}
|
||||||
|
prog.SetTotal(100)
|
||||||
|
prog.Set(100)
|
||||||
|
prog.SetCompleted()
|
||||||
|
|
||||||
|
files := progress.NewManual(-1)
|
||||||
|
files.SetCompleted()
|
||||||
|
|
||||||
|
mon := cataloger.Monitor{
|
||||||
|
FilesProcessed: files,
|
||||||
|
PackagesDiscovered: prog,
|
||||||
|
}
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: syftEvent.PackageCatalogerStarted,
|
||||||
|
Value: mon,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
event := tt.eventFn(t)
|
||||||
|
handler := New(DefaultHandlerConfig())
|
||||||
|
handler.WindowSize = tea.WindowSizeMsg{
|
||||||
|
Width: 100,
|
||||||
|
Height: 80,
|
||||||
|
}
|
||||||
|
|
||||||
|
models := handler.Handle(event)
|
||||||
|
require.Len(t, models, 1)
|
||||||
|
model := models[0]
|
||||||
|
|
||||||
|
tsk, ok := model.(taskprogress.Model)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||||
|
Time: time.Now(),
|
||||||
|
Sequence: tsk.Sequence(),
|
||||||
|
ID: tsk.ID(),
|
||||||
|
})
|
||||||
|
t.Log(got)
|
||||||
|
snaps.MatchSnapshot(t, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
201
cmd/syft/cli/ui/handle_pull_docker_image.go
Normal file
201
cmd/syft/cli/ui/handle_pull_docker_image.go
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
"github.com/wagoodman/go-progress"
|
||||||
|
|
||||||
|
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||||
|
stereoscopeParsers "github.com/anchore/stereoscope/pkg/event/parsers"
|
||||||
|
"github.com/anchore/stereoscope/pkg/image/docker"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ interface {
|
||||||
|
progress.Stager
|
||||||
|
progress.Progressable
|
||||||
|
} = (*dockerPullProgressAdapter)(nil)
|
||||||
|
|
||||||
|
type dockerPullStatus interface {
|
||||||
|
Complete() bool
|
||||||
|
Layers() []docker.LayerID
|
||||||
|
Current(docker.LayerID) docker.LayerState
|
||||||
|
}
|
||||||
|
|
||||||
|
type dockerPullProgressAdapter struct {
|
||||||
|
status dockerPullStatus
|
||||||
|
formatter dockerPullStatusFormatter
|
||||||
|
}
|
||||||
|
|
||||||
|
type dockerPullStatusFormatter struct {
|
||||||
|
auxInfoStyle lipgloss.Style
|
||||||
|
dockerPullCompletedStyle lipgloss.Style
|
||||||
|
dockerPullDownloadStyle lipgloss.Style
|
||||||
|
dockerPullExtractStyle lipgloss.Style
|
||||||
|
dockerPullStageChars []string
|
||||||
|
layerCaps []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Handler) handlePullDockerImage(e partybus.Event) []tea.Model {
|
||||||
|
_, pullStatus, err := stereoscopeParsers.ParsePullDockerImage(e)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("error", err).Warn("unable to parse event")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tsk := m.newTaskProgress(
|
||||||
|
taskprogress.Title{
|
||||||
|
Default: "Pull image",
|
||||||
|
Running: "Pulling image",
|
||||||
|
Success: "Pulled image",
|
||||||
|
},
|
||||||
|
taskprogress.WithStagedProgressable(
|
||||||
|
newDockerPullProgressAdapter(pullStatus),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
tsk.HintStyle = lipgloss.NewStyle()
|
||||||
|
tsk.HintEndCaps = nil
|
||||||
|
|
||||||
|
return []tea.Model{tsk}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDockerPullProgressAdapter(status dockerPullStatus) *dockerPullProgressAdapter {
|
||||||
|
return &dockerPullProgressAdapter{
|
||||||
|
status: status,
|
||||||
|
formatter: newDockerPullStatusFormatter(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDockerPullStatusFormatter() dockerPullStatusFormatter {
|
||||||
|
return dockerPullStatusFormatter{
|
||||||
|
auxInfoStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")),
|
||||||
|
dockerPullCompletedStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#fcba03")),
|
||||||
|
dockerPullDownloadStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")),
|
||||||
|
dockerPullExtractStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#ffffff")),
|
||||||
|
dockerPullStageChars: strings.Split("▁▃▄▅▆▇█", ""),
|
||||||
|
layerCaps: strings.Split("▕▏", ""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d dockerPullProgressAdapter) Size() int64 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d dockerPullProgressAdapter) Current() int64 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d dockerPullProgressAdapter) Error() error {
|
||||||
|
if d.status.Complete() {
|
||||||
|
return progress.ErrCompleted
|
||||||
|
}
|
||||||
|
// TODO: return intermediate error indications
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d dockerPullProgressAdapter) Stage() string {
|
||||||
|
return d.formatter.Render(d.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render crafts the given docker image pull status summarized into a single line.
|
||||||
|
func (f dockerPullStatusFormatter) Render(pullStatus dockerPullStatus) string {
|
||||||
|
var size, current uint64
|
||||||
|
|
||||||
|
layers := pullStatus.Layers()
|
||||||
|
status := make(map[docker.LayerID]docker.LayerState)
|
||||||
|
completed := make([]string, len(layers))
|
||||||
|
|
||||||
|
// fetch the current state
|
||||||
|
for idx, layer := range layers {
|
||||||
|
completed[idx] = " "
|
||||||
|
status[layer] = pullStatus.Current(layer)
|
||||||
|
}
|
||||||
|
|
||||||
|
numCompleted := 0
|
||||||
|
for idx, layer := range layers {
|
||||||
|
prog := status[layer].PhaseProgress
|
||||||
|
curN := prog.Current()
|
||||||
|
curSize := prog.Size()
|
||||||
|
|
||||||
|
if progress.IsCompleted(prog) {
|
||||||
|
input := f.dockerPullStageChars[len(f.dockerPullStageChars)-1]
|
||||||
|
completed[idx] = f.formatDockerPullPhase(status[layer].Phase, input)
|
||||||
|
} else if curN != 0 {
|
||||||
|
var ratio float64
|
||||||
|
switch {
|
||||||
|
case curN == 0 || curSize < 0:
|
||||||
|
ratio = 0
|
||||||
|
case curN >= curSize:
|
||||||
|
ratio = 1
|
||||||
|
default:
|
||||||
|
ratio = float64(curN) / float64(curSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
i := int(ratio * float64(len(f.dockerPullStageChars)-1))
|
||||||
|
input := f.dockerPullStageChars[i]
|
||||||
|
completed[idx] = f.formatDockerPullPhase(status[layer].Phase, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
if progress.IsErrCompleted(status[layer].DownloadProgress.Error()) {
|
||||||
|
numCompleted++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, layer := range layers {
|
||||||
|
prog := status[layer].DownloadProgress
|
||||||
|
size += uint64(prog.Size())
|
||||||
|
current += uint64(prog.Current())
|
||||||
|
}
|
||||||
|
|
||||||
|
var progStr, auxInfo string
|
||||||
|
if len(layers) > 0 {
|
||||||
|
render := strings.Join(completed, "")
|
||||||
|
prefix := f.dockerPullCompletedStyle.Render(fmt.Sprintf("%d Layers", len(layers)))
|
||||||
|
auxInfo = f.auxInfoStyle.Render(fmt.Sprintf("[%s / %s]", humanize.Bytes(current), humanize.Bytes(size)))
|
||||||
|
if len(layers) == numCompleted {
|
||||||
|
auxInfo = f.auxInfoStyle.Render(fmt.Sprintf("[%s] Extracting...", humanize.Bytes(size)))
|
||||||
|
}
|
||||||
|
|
||||||
|
progStr = fmt.Sprintf("%s%s%s%s", prefix, f.layerCap(false), render, f.layerCap(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
return progStr + auxInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatDockerPullPhase returns a single character that represents the status of a layer pull.
|
||||||
|
func (f dockerPullStatusFormatter) formatDockerPullPhase(phase docker.PullPhase, inputStr string) string {
|
||||||
|
switch phase {
|
||||||
|
case docker.WaitingPhase:
|
||||||
|
// ignore any progress related to waiting
|
||||||
|
return " "
|
||||||
|
case docker.PullingFsPhase, docker.DownloadingPhase:
|
||||||
|
return f.dockerPullDownloadStyle.Render(inputStr)
|
||||||
|
case docker.DownloadCompletePhase:
|
||||||
|
return f.dockerPullDownloadStyle.Render(f.dockerPullStageChars[len(f.dockerPullStageChars)-1])
|
||||||
|
case docker.ExtractingPhase:
|
||||||
|
return f.dockerPullExtractStyle.Render(inputStr)
|
||||||
|
case docker.VerifyingChecksumPhase, docker.PullCompletePhase:
|
||||||
|
return f.dockerPullCompletedStyle.Render(inputStr)
|
||||||
|
case docker.AlreadyExistsPhase:
|
||||||
|
return f.dockerPullCompletedStyle.Render(f.dockerPullStageChars[len(f.dockerPullStageChars)-1])
|
||||||
|
default:
|
||||||
|
return inputStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f dockerPullStatusFormatter) layerCap(end bool) string {
|
||||||
|
l := len(f.layerCaps)
|
||||||
|
if l == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if end {
|
||||||
|
return f.layerCaps[l-1]
|
||||||
|
}
|
||||||
|
return f.layerCaps[0]
|
||||||
|
}
|
163
cmd/syft/cli/ui/handle_pull_docker_image_test.go
Normal file
163
cmd/syft/cli/ui/handle_pull_docker_image_test.go
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gkampitakis/go-snaps/snaps"
|
||||||
|
"github.com/wagoodman/go-progress"
|
||||||
|
|
||||||
|
"github.com/anchore/stereoscope/pkg/image/docker"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ dockerPullStatus = (*mockDockerPullStatus)(nil)
|
||||||
|
|
||||||
|
type mockDockerPullStatus struct {
|
||||||
|
complete bool
|
||||||
|
layers []docker.LayerID
|
||||||
|
current map[docker.LayerID]docker.LayerState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockDockerPullStatus) Complete() bool {
|
||||||
|
return m.complete
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockDockerPullStatus) Layers() []docker.LayerID {
|
||||||
|
return m.layers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mockDockerPullStatus) Current(id docker.LayerID) docker.LayerState {
|
||||||
|
return m.current[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_dockerPullStatusFormatter_Render(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
status dockerPullStatus
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "pulling",
|
||||||
|
status: func() dockerPullStatus {
|
||||||
|
complete := progress.NewManual(10)
|
||||||
|
complete.Set(10)
|
||||||
|
complete.SetCompleted()
|
||||||
|
|
||||||
|
quarter := progress.NewManual(10)
|
||||||
|
quarter.Set(2)
|
||||||
|
|
||||||
|
half := progress.NewManual(10)
|
||||||
|
half.Set(6)
|
||||||
|
|
||||||
|
empty := progress.NewManual(10)
|
||||||
|
|
||||||
|
return mockDockerPullStatus{
|
||||||
|
complete: false,
|
||||||
|
layers: []docker.LayerID{
|
||||||
|
"sha256:1",
|
||||||
|
"sha256:2",
|
||||||
|
"sha256:3",
|
||||||
|
},
|
||||||
|
current: map[docker.LayerID]docker.LayerState{
|
||||||
|
"sha256:1": {
|
||||||
|
Phase: docker.ExtractingPhase,
|
||||||
|
PhaseProgress: half,
|
||||||
|
DownloadProgress: complete,
|
||||||
|
},
|
||||||
|
"sha256:2": {
|
||||||
|
Phase: docker.DownloadingPhase,
|
||||||
|
PhaseProgress: quarter,
|
||||||
|
DownloadProgress: quarter,
|
||||||
|
},
|
||||||
|
"sha256:3": {
|
||||||
|
Phase: docker.WaitingPhase,
|
||||||
|
PhaseProgress: empty,
|
||||||
|
DownloadProgress: empty,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "download complete",
|
||||||
|
status: func() dockerPullStatus {
|
||||||
|
complete := progress.NewManual(10)
|
||||||
|
complete.Set(10)
|
||||||
|
complete.SetCompleted()
|
||||||
|
|
||||||
|
quarter := progress.NewManual(10)
|
||||||
|
quarter.Set(2)
|
||||||
|
|
||||||
|
half := progress.NewManual(10)
|
||||||
|
half.Set(6)
|
||||||
|
|
||||||
|
empty := progress.NewManual(10)
|
||||||
|
|
||||||
|
return mockDockerPullStatus{
|
||||||
|
complete: false,
|
||||||
|
layers: []docker.LayerID{
|
||||||
|
"sha256:1",
|
||||||
|
"sha256:2",
|
||||||
|
"sha256:3",
|
||||||
|
},
|
||||||
|
current: map[docker.LayerID]docker.LayerState{
|
||||||
|
"sha256:1": {
|
||||||
|
Phase: docker.ExtractingPhase,
|
||||||
|
PhaseProgress: complete,
|
||||||
|
DownloadProgress: complete,
|
||||||
|
},
|
||||||
|
"sha256:2": {
|
||||||
|
Phase: docker.ExtractingPhase,
|
||||||
|
PhaseProgress: quarter,
|
||||||
|
DownloadProgress: complete,
|
||||||
|
},
|
||||||
|
"sha256:3": {
|
||||||
|
Phase: docker.ExtractingPhase,
|
||||||
|
PhaseProgress: empty,
|
||||||
|
DownloadProgress: complete,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complete",
|
||||||
|
status: func() dockerPullStatus {
|
||||||
|
complete := progress.NewManual(10)
|
||||||
|
complete.Set(10)
|
||||||
|
complete.SetCompleted()
|
||||||
|
|
||||||
|
return mockDockerPullStatus{
|
||||||
|
complete: true,
|
||||||
|
layers: []docker.LayerID{
|
||||||
|
"sha256:1",
|
||||||
|
"sha256:2",
|
||||||
|
"sha256:3",
|
||||||
|
},
|
||||||
|
current: map[docker.LayerID]docker.LayerState{
|
||||||
|
"sha256:1": {
|
||||||
|
Phase: docker.PullCompletePhase,
|
||||||
|
PhaseProgress: complete,
|
||||||
|
DownloadProgress: complete,
|
||||||
|
},
|
||||||
|
"sha256:2": {
|
||||||
|
Phase: docker.PullCompletePhase,
|
||||||
|
PhaseProgress: complete,
|
||||||
|
DownloadProgress: complete,
|
||||||
|
},
|
||||||
|
"sha256:3": {
|
||||||
|
Phase: docker.PullCompletePhase,
|
||||||
|
PhaseProgress: complete,
|
||||||
|
DownloadProgress: complete,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
f := newDockerPullStatusFormatter()
|
||||||
|
snaps.MatchSnapshot(t, f.Render(tt.status))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
33
cmd/syft/cli/ui/handle_read_image.go
Normal file
33
cmd/syft/cli/ui/handle_read_image.go
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
|
||||||
|
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||||
|
stereoEventParsers "github.com/anchore/stereoscope/pkg/event/parsers"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Handler) handleReadImage(e partybus.Event) []tea.Model {
|
||||||
|
imgMetadata, prog, err := stereoEventParsers.ParseReadImage(e)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("error", err).Warn("unable to parse event")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tsk := m.newTaskProgress(
|
||||||
|
taskprogress.Title{
|
||||||
|
Default: "Parse image",
|
||||||
|
Running: "Parsing image",
|
||||||
|
Success: "Parsed image",
|
||||||
|
},
|
||||||
|
taskprogress.WithProgress(prog),
|
||||||
|
)
|
||||||
|
|
||||||
|
if imgMetadata != nil {
|
||||||
|
tsk.Context = []string{imgMetadata.ID}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []tea.Model{tsk}
|
||||||
|
}
|
117
cmd/syft/cli/ui/handle_read_image_test.go
Normal file
117
cmd/syft/cli/ui/handle_read_image_test.go
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/gkampitakis/go-snaps/snaps"
|
||||||
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
"github.com/wagoodman/go-progress"
|
||||||
|
|
||||||
|
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||||
|
stereoscopeEvent "github.com/anchore/stereoscope/pkg/event"
|
||||||
|
"github.com/anchore/stereoscope/pkg/image"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandler_handleReadImage(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
eventFn func(*testing.T) partybus.Event
|
||||||
|
iterations int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "read image in progress",
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
prog := &progress.Manual{}
|
||||||
|
prog.SetTotal(100)
|
||||||
|
prog.Set(50)
|
||||||
|
|
||||||
|
src := image.Metadata{
|
||||||
|
ID: "id",
|
||||||
|
Size: 42,
|
||||||
|
Config: v1.ConfigFile{
|
||||||
|
Architecture: "arch",
|
||||||
|
Author: "auth",
|
||||||
|
Container: "cont",
|
||||||
|
OS: "os",
|
||||||
|
OSVersion: "os-ver",
|
||||||
|
Variant: "vari",
|
||||||
|
},
|
||||||
|
MediaType: "media",
|
||||||
|
ManifestDigest: "digest",
|
||||||
|
Architecture: "arch",
|
||||||
|
Variant: "var",
|
||||||
|
OS: "os",
|
||||||
|
}
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: stereoscopeEvent.ReadImage,
|
||||||
|
Source: src,
|
||||||
|
Value: prog,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "read image complete",
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
prog := &progress.Manual{}
|
||||||
|
prog.SetTotal(100)
|
||||||
|
prog.Set(100)
|
||||||
|
prog.SetCompleted()
|
||||||
|
|
||||||
|
src := image.Metadata{
|
||||||
|
ID: "id",
|
||||||
|
Size: 42,
|
||||||
|
Config: v1.ConfigFile{
|
||||||
|
Architecture: "arch",
|
||||||
|
Author: "auth",
|
||||||
|
Container: "cont",
|
||||||
|
OS: "os",
|
||||||
|
OSVersion: "os-ver",
|
||||||
|
Variant: "vari",
|
||||||
|
},
|
||||||
|
MediaType: "media",
|
||||||
|
ManifestDigest: "digest",
|
||||||
|
Architecture: "arch",
|
||||||
|
Variant: "var",
|
||||||
|
OS: "os",
|
||||||
|
}
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: stereoscopeEvent.ReadImage,
|
||||||
|
Source: src,
|
||||||
|
Value: prog,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
event := tt.eventFn(t)
|
||||||
|
handler := New(DefaultHandlerConfig())
|
||||||
|
handler.WindowSize = tea.WindowSizeMsg{
|
||||||
|
Width: 100,
|
||||||
|
Height: 80,
|
||||||
|
}
|
||||||
|
|
||||||
|
models := handler.Handle(event)
|
||||||
|
require.Len(t, models, 1)
|
||||||
|
model := models[0]
|
||||||
|
|
||||||
|
tsk, ok := model.(taskprogress.Model)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||||
|
Time: time.Now(),
|
||||||
|
Sequence: tsk.Sequence(),
|
||||||
|
ID: tsk.ID(),
|
||||||
|
})
|
||||||
|
t.Log(got)
|
||||||
|
snaps.MatchSnapshot(t, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
57
cmd/syft/cli/ui/handle_secrets_cataloger.go
Normal file
57
cmd/syft/cli/ui/handle_secrets_cataloger.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
"github.com/wagoodman/go-progress"
|
||||||
|
|
||||||
|
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||||
|
"github.com/anchore/syft/syft/file/cataloger/secrets"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ progress.StagedProgressable = (*secretsCatalogerProgressAdapter)(nil)
|
||||||
|
|
||||||
|
// Deprecated: will be removed in syft 1.0
|
||||||
|
type secretsCatalogerProgressAdapter struct {
|
||||||
|
*secrets.Monitor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: will be removed in syft 1.0
|
||||||
|
func newSecretsCatalogerProgressAdapter(monitor *secrets.Monitor) secretsCatalogerProgressAdapter {
|
||||||
|
return secretsCatalogerProgressAdapter{
|
||||||
|
Monitor: monitor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s secretsCatalogerProgressAdapter) Stage() string {
|
||||||
|
return fmt.Sprintf("%d secrets", s.Monitor.SecretsDiscovered.Current())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: will be removed in syft 1.0
|
||||||
|
func (m *Handler) handleSecretsCatalogerStarted(e partybus.Event) []tea.Model {
|
||||||
|
mon, err := syftEventParsers.ParseSecretsCatalogingStarted(e)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("error", err).Warn("unable to parse event")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tsk := m.newTaskProgress(
|
||||||
|
taskprogress.Title{
|
||||||
|
Default: "Catalog secrets",
|
||||||
|
Running: "Cataloging secrets",
|
||||||
|
Success: "Cataloged secrets",
|
||||||
|
},
|
||||||
|
|
||||||
|
taskprogress.WithStagedProgressable(
|
||||||
|
newSecretsCatalogerProgressAdapter(mon),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
tsk.HideStageOnSuccess = false
|
||||||
|
|
||||||
|
return []tea.Model{tsk}
|
||||||
|
}
|
96
cmd/syft/cli/ui/handle_secrets_cataloger_test.go
Normal file
96
cmd/syft/cli/ui/handle_secrets_cataloger_test.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/gkampitakis/go-snaps/snaps"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
"github.com/wagoodman/go-progress"
|
||||||
|
|
||||||
|
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||||
|
syftEvent "github.com/anchore/syft/syft/event"
|
||||||
|
"github.com/anchore/syft/syft/file/cataloger/secrets"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandler_handleSecretsCatalogerStarted(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
eventFn func(*testing.T) partybus.Event
|
||||||
|
iterations int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "cataloging in progress",
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
stage := &progress.Stage{
|
||||||
|
Current: "current",
|
||||||
|
}
|
||||||
|
secretsDiscovered := progress.NewManual(-1)
|
||||||
|
secretsDiscovered.Set(64)
|
||||||
|
prog := progress.NewManual(72)
|
||||||
|
prog.Set(50)
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: syftEvent.SecretsCatalogerStarted,
|
||||||
|
Source: secretsDiscovered,
|
||||||
|
Value: secrets.Monitor{
|
||||||
|
Stager: progress.Stager(stage),
|
||||||
|
SecretsDiscovered: secretsDiscovered,
|
||||||
|
Progressable: prog,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cataloging complete",
|
||||||
|
eventFn: func(t *testing.T) partybus.Event {
|
||||||
|
stage := &progress.Stage{
|
||||||
|
Current: "current",
|
||||||
|
}
|
||||||
|
secretsDiscovered := progress.NewManual(-1)
|
||||||
|
secretsDiscovered.Set(64)
|
||||||
|
prog := progress.NewManual(72)
|
||||||
|
prog.Set(72)
|
||||||
|
prog.SetCompleted()
|
||||||
|
|
||||||
|
return partybus.Event{
|
||||||
|
Type: syftEvent.SecretsCatalogerStarted,
|
||||||
|
Source: secretsDiscovered,
|
||||||
|
Value: secrets.Monitor{
|
||||||
|
Stager: progress.Stager(stage),
|
||||||
|
SecretsDiscovered: secretsDiscovered,
|
||||||
|
Progressable: prog,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
event := tt.eventFn(t)
|
||||||
|
handler := New(DefaultHandlerConfig())
|
||||||
|
handler.WindowSize = tea.WindowSizeMsg{
|
||||||
|
Width: 100,
|
||||||
|
Height: 80,
|
||||||
|
}
|
||||||
|
|
||||||
|
models := handler.Handle(event)
|
||||||
|
require.Len(t, models, 1)
|
||||||
|
model := models[0]
|
||||||
|
|
||||||
|
tsk, ok := model.(taskprogress.Model)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{
|
||||||
|
Time: time.Now(),
|
||||||
|
Sequence: tsk.Sequence(),
|
||||||
|
ID: tsk.ID(),
|
||||||
|
})
|
||||||
|
t.Log(got)
|
||||||
|
snaps.MatchSnapshot(t, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
68
cmd/syft/cli/ui/handler.go
Normal file
68
cmd/syft/cli/ui/handler.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
|
||||||
|
"github.com/anchore/bubbly"
|
||||||
|
"github.com/anchore/bubbly/bubbles/taskprogress"
|
||||||
|
stereoscopeEvent "github.com/anchore/stereoscope/pkg/event"
|
||||||
|
syftEvent "github.com/anchore/syft/syft/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ bubbly.EventHandler = (*Handler)(nil)
|
||||||
|
|
||||||
|
type HandlerConfig struct {
|
||||||
|
TitleWidth int
|
||||||
|
AdjustDefaultTask func(taskprogress.Model) taskprogress.Model
|
||||||
|
}
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
WindowSize tea.WindowSizeMsg
|
||||||
|
Running *sync.WaitGroup
|
||||||
|
Config HandlerConfig
|
||||||
|
|
||||||
|
bubbly.EventHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultHandlerConfig() HandlerConfig {
|
||||||
|
return HandlerConfig{
|
||||||
|
TitleWidth: 30,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg HandlerConfig) *Handler {
|
||||||
|
d := bubbly.NewEventDispatcher()
|
||||||
|
|
||||||
|
h := &Handler{
|
||||||
|
EventHandler: d,
|
||||||
|
Running: &sync.WaitGroup{},
|
||||||
|
Config: cfg,
|
||||||
|
}
|
||||||
|
|
||||||
|
// register all supported event types with the respective handler functions
|
||||||
|
d.AddHandlers(map[partybus.EventType]bubbly.EventHandlerFn{
|
||||||
|
stereoscopeEvent.PullDockerImage: h.handlePullDockerImage,
|
||||||
|
stereoscopeEvent.ReadImage: h.handleReadImage,
|
||||||
|
stereoscopeEvent.FetchImage: h.handleFetchImage,
|
||||||
|
syftEvent.PackageCatalogerStarted: h.handlePackageCatalogerStarted,
|
||||||
|
syftEvent.FileDigestsCatalogerStarted: h.handleFileDigestsCatalogerStarted,
|
||||||
|
syftEvent.FileMetadataCatalogerStarted: h.handleFileMetadataCatalogerStarted,
|
||||||
|
syftEvent.FileIndexingStarted: h.handleFileIndexingStarted,
|
||||||
|
syftEvent.AttestationStarted: h.handleAttestationStarted,
|
||||||
|
syftEvent.CatalogerTaskStarted: h.handleCatalogerTaskStarted,
|
||||||
|
|
||||||
|
// deprecated
|
||||||
|
syftEvent.SecretsCatalogerStarted: h.handleSecretsCatalogerStarted,
|
||||||
|
})
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Handler) Update(msg tea.Msg) {
|
||||||
|
if msg, ok := msg.(tea.WindowSizeMsg); ok {
|
||||||
|
m.WindowSize = msg
|
||||||
|
}
|
||||||
|
}
|
19
cmd/syft/cli/ui/new_task_progress.go
Normal file
19
cmd/syft/cli/ui/new_task_progress.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import "github.com/anchore/bubbly/bubbles/taskprogress"
|
||||||
|
|
||||||
|
func (m Handler) newTaskProgress(title taskprogress.Title, opts ...taskprogress.Option) taskprogress.Model {
|
||||||
|
tsk := taskprogress.New(m.Running, opts...)
|
||||||
|
|
||||||
|
tsk.HideProgressOnSuccess = true
|
||||||
|
tsk.HideStageOnSuccess = true
|
||||||
|
tsk.WindowSize = m.WindowSize
|
||||||
|
tsk.TitleWidth = m.Config.TitleWidth
|
||||||
|
tsk.TitleOptions = title
|
||||||
|
|
||||||
|
if m.Config.AdjustDefaultTask != nil {
|
||||||
|
tsk = m.Config.AdjustDefaultTask(tsk)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tsk
|
||||||
|
}
|
62
cmd/syft/cli/ui/util_test.go
Normal file
62
cmd/syft/cli/ui/util_test.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runModel(t testing.TB, m tea.Model, iterations int, message tea.Msg) string {
|
||||||
|
t.Helper()
|
||||||
|
if iterations == 0 {
|
||||||
|
iterations = 1
|
||||||
|
}
|
||||||
|
m.Init()
|
||||||
|
var cmd tea.Cmd = func() tea.Msg {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; cmd != nil && i < iterations; i++ {
|
||||||
|
msgs := flatten(cmd())
|
||||||
|
var nextCmds []tea.Cmd
|
||||||
|
var next tea.Cmd
|
||||||
|
for _, msg := range msgs {
|
||||||
|
t.Logf("Message: %+v %+v\n", reflect.TypeOf(msg), msg)
|
||||||
|
m, next = m.Update(msg)
|
||||||
|
nextCmds = append(nextCmds, next)
|
||||||
|
}
|
||||||
|
cmd = tea.Batch(nextCmds...)
|
||||||
|
}
|
||||||
|
return m.View()
|
||||||
|
}
|
||||||
|
|
||||||
|
func flatten(p tea.Msg) (msgs []tea.Msg) {
|
||||||
|
if reflect.TypeOf(p).Name() == "batchMsg" {
|
||||||
|
partials := extractBatchMessages(p)
|
||||||
|
for _, m := range partials {
|
||||||
|
msgs = append(msgs, flatten(m)...)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
msgs = []tea.Msg{p}
|
||||||
|
}
|
||||||
|
return msgs
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractBatchMessages(m tea.Msg) (ret []tea.Msg) {
|
||||||
|
sliceMsgType := reflect.SliceOf(reflect.TypeOf(tea.Cmd(nil)))
|
||||||
|
value := reflect.ValueOf(m) // note: this is technically unaddressable
|
||||||
|
|
||||||
|
// make our own instance that is addressable
|
||||||
|
valueCopy := reflect.New(value.Type()).Elem()
|
||||||
|
valueCopy.Set(value)
|
||||||
|
|
||||||
|
cmds := reflect.NewAt(sliceMsgType, unsafe.Pointer(valueCopy.UnsafeAddr())).Elem()
|
||||||
|
for i := 0; i < cmds.Len(); i++ {
|
||||||
|
item := cmds.Index(i)
|
||||||
|
r := item.Call(nil)
|
||||||
|
ret = append(ret, r[0].Interface().(tea.Msg))
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
46
cmd/syft/internal/ui/__snapshots__/post_ui_event_writer_test.snap
Executable file
46
cmd/syft/internal/ui/__snapshots__/post_ui_event_writer_test.snap
Executable file
|
@ -0,0 +1,46 @@
|
||||||
|
|
||||||
|
[Test_postUIEventWriter_write/no_events/stdout - 1]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[Test_postUIEventWriter_write/no_events/stderr - 1]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[Test_postUIEventWriter_write/all_events/stdout - 1]
|
||||||
|
|
||||||
|
|
||||||
|
<my --
|
||||||
|
-
|
||||||
|
-
|
||||||
|
report 1!!>
|
||||||
|
<report 2>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[Test_postUIEventWriter_write/all_events/stderr - 1]
|
||||||
|
|
||||||
|
|
||||||
|
<my notification 1!!
|
||||||
|
...still notifying>
|
||||||
|
|
||||||
|
|
||||||
|
<notification 2>
|
||||||
|
<notification 3>
|
||||||
|
|
||||||
|
|
||||||
|
<my app can be updated!!
|
||||||
|
...to this version>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[Test_postUIEventWriter_write/quiet_only_shows_report/stdout - 1]
|
||||||
|
<report 1>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[Test_postUIEventWriter_write/quiet_only_shows_report/stderr - 1]
|
||||||
|
|
||||||
|
---
|
44
cmd/syft/internal/ui/no_ui.go
Normal file
44
cmd/syft/internal/ui/no_ui.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
|
||||||
|
"github.com/anchore/clio"
|
||||||
|
"github.com/anchore/syft/syft/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ clio.UI = (*NoUI)(nil)
|
||||||
|
|
||||||
|
type NoUI struct {
|
||||||
|
finalizeEvents []partybus.Event
|
||||||
|
subscription partybus.Unsubscribable
|
||||||
|
quiet bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func None(quiet bool) *NoUI {
|
||||||
|
return &NoUI{
|
||||||
|
quiet: quiet,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NoUI) Setup(subscription partybus.Unsubscribable) error {
|
||||||
|
n.subscription = subscription
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NoUI) Handle(e partybus.Event) error {
|
||||||
|
switch e.Type {
|
||||||
|
case event.CLIReport, event.CLINotification:
|
||||||
|
// keep these for when the UI is terminated to show to the screen (or perform other events)
|
||||||
|
n.finalizeEvents = append(n.finalizeEvents, e)
|
||||||
|
case event.CLIExit:
|
||||||
|
return n.subscription.Unsubscribe()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n NoUI) Teardown(_ bool) error {
|
||||||
|
return newPostUIEventWriter(os.Stdout, os.Stderr).write(n.quiet, n.finalizeEvents...)
|
||||||
|
}
|
133
cmd/syft/internal/ui/post_ui_event_writer.go
Normal file
133
cmd/syft/internal/ui/post_ui_event_writer.go
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft/event"
|
||||||
|
"github.com/anchore/syft/syft/event/parsers"
|
||||||
|
)
|
||||||
|
|
||||||
|
type postUIEventWriter struct {
|
||||||
|
handles []postUIHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
type postUIHandle struct {
|
||||||
|
respectQuiet bool
|
||||||
|
event partybus.EventType
|
||||||
|
writer io.Writer
|
||||||
|
dispatch eventWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
type eventWriter func(io.Writer, ...partybus.Event) error
|
||||||
|
|
||||||
|
func newPostUIEventWriter(stdout, stderr io.Writer) *postUIEventWriter {
|
||||||
|
return &postUIEventWriter{
|
||||||
|
handles: []postUIHandle{
|
||||||
|
{
|
||||||
|
event: event.CLIReport,
|
||||||
|
respectQuiet: false,
|
||||||
|
writer: stdout,
|
||||||
|
dispatch: writeReports,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: event.CLINotification,
|
||||||
|
respectQuiet: true,
|
||||||
|
writer: stderr,
|
||||||
|
dispatch: writeNotifications,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: event.CLIAppUpdateAvailable,
|
||||||
|
respectQuiet: true,
|
||||||
|
writer: stderr,
|
||||||
|
dispatch: writeAppUpdate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w postUIEventWriter) write(quiet bool, events ...partybus.Event) error {
|
||||||
|
var errs error
|
||||||
|
for _, h := range w.handles {
|
||||||
|
if quiet && h.respectQuiet {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range events {
|
||||||
|
if e.Type != h.event {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.dispatch(h.writer, e); err != nil {
|
||||||
|
errs = multierror.Append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeReports(writer io.Writer, events ...partybus.Event) error {
|
||||||
|
var reports []string
|
||||||
|
for _, e := range events {
|
||||||
|
_, report, err := parsers.ParseCLIReport(e)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("error", err).Warn("failed to gather final report")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove all whitespace padding from the end of the report
|
||||||
|
reports = append(reports, strings.TrimRight(report, "\n ")+"\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// prevent the double new-line at the end of the report
|
||||||
|
report := strings.Join(reports, "\n")
|
||||||
|
|
||||||
|
if _, err := fmt.Fprint(writer, report); err != nil {
|
||||||
|
return fmt.Errorf("failed to write final report to stdout: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeNotifications(writer io.Writer, events ...partybus.Event) error {
|
||||||
|
// 13 = high intensity magenta (ANSI 16 bit code)
|
||||||
|
style := lipgloss.NewStyle().Foreground(lipgloss.Color("13"))
|
||||||
|
|
||||||
|
for _, e := range events {
|
||||||
|
_, notification, err := parsers.ParseCLINotification(e)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("error", err).Warn("failed to parse notification")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := fmt.Fprintln(writer, style.Render(notification)); err != nil {
|
||||||
|
// don't let this be fatal
|
||||||
|
log.WithFields("error", err).Warn("failed to write final notifications")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeAppUpdate(writer io.Writer, events ...partybus.Event) error {
|
||||||
|
// 13 = high intensity magenta (ANSI 16 bit code) + italics
|
||||||
|
style := lipgloss.NewStyle().Foreground(lipgloss.Color("13")).Italic(true)
|
||||||
|
|
||||||
|
for _, e := range events {
|
||||||
|
notice, err := parsers.ParseCLIAppUpdateAvailable(e)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("error", err).Warn("failed to parse app update notification")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := fmt.Fprintln(writer, style.Render(notice)); err != nil {
|
||||||
|
// don't let this be fatal
|
||||||
|
log.WithFields("error", err).Warn("failed to write app update notification")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
95
cmd/syft/internal/ui/post_ui_event_writer_test.go
Normal file
95
cmd/syft/internal/ui/post_ui_event_writer_test.go
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gkampitakis/go-snaps/snaps"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_postUIEventWriter_write(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
quiet bool
|
||||||
|
events []partybus.Event
|
||||||
|
wantErr require.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no events",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all events",
|
||||||
|
events: []partybus.Event{
|
||||||
|
{
|
||||||
|
Type: event.CLINotification,
|
||||||
|
Value: "\n\n<my notification 1!!\n...still notifying>\n\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: event.CLINotification,
|
||||||
|
Value: "<notification 2>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: event.CLIAppUpdateAvailable,
|
||||||
|
Value: "\n\n<my app can be updated!!\n...to this version>\n\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: event.CLINotification,
|
||||||
|
Value: "<notification 3>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: event.CLIReport,
|
||||||
|
Value: "\n\n<my --\n-\n-\nreport 1!!>\n\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: event.CLIReport,
|
||||||
|
Value: "<report 2>",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "quiet only shows report",
|
||||||
|
quiet: true,
|
||||||
|
events: []partybus.Event{
|
||||||
|
|
||||||
|
{
|
||||||
|
Type: event.CLINotification,
|
||||||
|
Value: "<notification 1>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: event.CLIAppUpdateAvailable,
|
||||||
|
Value: "<app update>",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: event.CLIReport,
|
||||||
|
Value: "<report 1>",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.wantErr == nil {
|
||||||
|
tt.wantErr = require.NoError
|
||||||
|
}
|
||||||
|
|
||||||
|
stdout := &bytes.Buffer{}
|
||||||
|
stderr := &bytes.Buffer{}
|
||||||
|
w := newPostUIEventWriter(stdout, stderr)
|
||||||
|
|
||||||
|
tt.wantErr(t, w.write(tt.quiet, tt.events...))
|
||||||
|
|
||||||
|
t.Run("stdout", func(t *testing.T) {
|
||||||
|
snaps.MatchSnapshot(t, stdout.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("stderr", func(t *testing.T) {
|
||||||
|
snaps.MatchSnapshot(t, stderr.String())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,9 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
|
|
||||||
|
"github.com/anchore/clio"
|
||||||
|
handler "github.com/anchore/syft/cmd/syft/cli/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Select is responsible for determining the specific UI function given select user option, the current platform
|
// Select is responsible for determining the specific UI function given select user option, the current platform
|
||||||
|
@ -15,16 +18,18 @@ import (
|
||||||
// is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there
|
// 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
|
// are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of
|
||||||
// the final SBOM report.
|
// the final SBOM report.
|
||||||
func Select(verbose, quiet bool) (uis []UI) {
|
func Select(verbose, quiet bool) (uis []clio.UI) {
|
||||||
isStdoutATty := term.IsTerminal(int(os.Stdout.Fd()))
|
isStdoutATty := term.IsTerminal(int(os.Stdout.Fd()))
|
||||||
isStderrATty := term.IsTerminal(int(os.Stderr.Fd()))
|
isStderrATty := term.IsTerminal(int(os.Stderr.Fd()))
|
||||||
notATerminal := !isStderrATty && !isStdoutATty
|
notATerminal := !isStderrATty && !isStdoutATty
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty:
|
case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty:
|
||||||
uis = append(uis, NewLoggerUI())
|
uis = append(uis, None(quiet))
|
||||||
default:
|
default:
|
||||||
uis = append(uis, NewEphemeralTerminalUI())
|
// TODO: it may make sense in the future to pass handler options into select
|
||||||
|
h := handler.New(handler.DefaultHandlerConfig())
|
||||||
|
uis = append(uis, New(h, verbose, quiet))
|
||||||
}
|
}
|
||||||
|
|
||||||
return uis
|
return uis
|
|
@ -3,11 +3,13 @@
|
||||||
|
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
|
import "github.com/anchore/clio"
|
||||||
|
|
||||||
// Select is responsible for determining the specific UI function given select user option, the current platform
|
// 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). The first UI in the returned slice of UIs
|
// 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
|
// 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
|
// are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of
|
||||||
// the final SBOM report.
|
// the final SBOM report.
|
||||||
func Select(verbose, quiet bool) (uis []UI) {
|
func Select(verbose, quiet bool) (uis []clio.UI) {
|
||||||
return append(uis, NewLoggerUI())
|
return append(uis, None(quiet))
|
||||||
}
|
}
|
163
cmd/syft/internal/ui/ui.go
Normal file
163
cmd/syft/internal/ui/ui.go
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
|
||||||
|
"github.com/anchore/bubbly/bubbles/frame"
|
||||||
|
"github.com/anchore/clio"
|
||||||
|
"github.com/anchore/go-logger"
|
||||||
|
handler "github.com/anchore/syft/cmd/syft/cli/ui"
|
||||||
|
"github.com/anchore/syft/internal/bus"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ interface {
|
||||||
|
tea.Model
|
||||||
|
partybus.Responder
|
||||||
|
clio.UI
|
||||||
|
} = (*UI)(nil)
|
||||||
|
|
||||||
|
type UI struct {
|
||||||
|
program *tea.Program
|
||||||
|
running *sync.WaitGroup
|
||||||
|
quiet bool
|
||||||
|
subscription partybus.Unsubscribable
|
||||||
|
finalizeEvents []partybus.Event
|
||||||
|
|
||||||
|
handler *handler.Handler
|
||||||
|
frame tea.Model
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(h *handler.Handler, _, quiet bool) *UI {
|
||||||
|
return &UI{
|
||||||
|
handler: h,
|
||||||
|
frame: frame.New(),
|
||||||
|
running: &sync.WaitGroup{},
|
||||||
|
quiet: quiet,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UI) Setup(subscription partybus.Unsubscribable) error {
|
||||||
|
// we still want to collect log messages, however, we also the logger shouldn't write to the screen directly
|
||||||
|
if logWrapper, ok := log.Get().(logger.Controller); ok {
|
||||||
|
logWrapper.SetOutput(m.frame.(*frame.Frame).Footer())
|
||||||
|
}
|
||||||
|
|
||||||
|
m.subscription = subscription
|
||||||
|
m.program = tea.NewProgram(m, tea.WithOutput(os.Stderr), tea.WithInput(os.Stdin))
|
||||||
|
m.running.Add(1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer m.running.Done()
|
||||||
|
if _, err := m.program.Run(); err != nil {
|
||||||
|
log.Errorf("unable to start UI: %+v", err)
|
||||||
|
m.exit()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UI) exit() {
|
||||||
|
// stop the event loop
|
||||||
|
bus.Exit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UI) Handle(e partybus.Event) error {
|
||||||
|
if m.program != nil {
|
||||||
|
m.program.Send(e)
|
||||||
|
if e.Type == event.CLIExit {
|
||||||
|
return m.subscription.Unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UI) Teardown(force bool) error {
|
||||||
|
if !force {
|
||||||
|
m.handler.Running.Wait()
|
||||||
|
m.program.Quit()
|
||||||
|
} else {
|
||||||
|
m.program.Kill()
|
||||||
|
}
|
||||||
|
|
||||||
|
m.running.Wait()
|
||||||
|
|
||||||
|
// TODO: allow for writing out the full log output to the screen (only a partial log is shown currently)
|
||||||
|
// this needs coordination to know what the last frame event is to change the state accordingly (which isn't possible now)
|
||||||
|
|
||||||
|
return newPostUIEventWriter(os.Stdout, os.Stderr).write(m.quiet, m.finalizeEvents...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// bubbletea.Model functions
|
||||||
|
|
||||||
|
func (m UI) Init() tea.Cmd {
|
||||||
|
return m.frame.Init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m UI) RespondsTo() []partybus.EventType {
|
||||||
|
return append([]partybus.EventType{
|
||||||
|
event.CLIReport,
|
||||||
|
event.CLINotification,
|
||||||
|
event.CLIExit,
|
||||||
|
event.CLIAppUpdateAvailable,
|
||||||
|
}, m.handler.RespondsTo()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
// note: we need a pointer receiver such that the same instance of UI used in Teardown is referenced (to keep finalize events)
|
||||||
|
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
|
||||||
|
// allow for non-partybus UI updates (such as window size events). Note: these must not affect existing models,
|
||||||
|
// that is the responsibility of the frame object on this UI object. The handler is a factory of models
|
||||||
|
// which the frame is responsible for the lifecycle of. This update allows for injecting the initial state
|
||||||
|
// of the world when creating those models.
|
||||||
|
m.handler.Update(msg)
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "esc", "ctrl+c":
|
||||||
|
m.exit()
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
|
||||||
|
case partybus.Event:
|
||||||
|
log.WithFields("component", "ui").Tracef("event: %q", msg.Type)
|
||||||
|
|
||||||
|
switch msg.Type {
|
||||||
|
case event.CLIReport, event.CLINotification, event.CLIExit, event.CLIAppUpdateAvailable:
|
||||||
|
// keep these for when the UI is terminated to show to the screen (or perform other events)
|
||||||
|
m.finalizeEvents = append(m.finalizeEvents, msg)
|
||||||
|
|
||||||
|
// why not return tea.Quit here for exit events? because there may be UI components that still need the update-render loop.
|
||||||
|
// for this reason we'll let the syft event loop call Teardown() which will explicitly wait for these components
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, newModel := range m.handler.Handle(msg) {
|
||||||
|
if newModel == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cmds = append(cmds, newModel.Init())
|
||||||
|
m.frame.(*frame.Frame).AppendModel(newModel)
|
||||||
|
}
|
||||||
|
// intentionally fallthrough to update the frame model
|
||||||
|
}
|
||||||
|
|
||||||
|
frameModel, cmd := m.frame.Update(msg)
|
||||||
|
m.frame = frameModel
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m UI) View() string {
|
||||||
|
return m.frame.View()
|
||||||
|
}
|
34
go.mod
34
go.mod
|
@ -38,7 +38,7 @@ require (
|
||||||
github.com/spf13/viper v1.16.0
|
github.com/spf13/viper v1.16.0
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
github.com/vifraa/gopom v0.2.1
|
github.com/vifraa/gopom v0.2.1
|
||||||
github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5
|
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651
|
||||||
github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5
|
github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5
|
||||||
github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb
|
github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0
|
github.com/xeipuuv/gojsonschema v1.2.0
|
||||||
|
@ -52,12 +52,17 @@ require (
|
||||||
github.com/CycloneDX/cyclonedx-go v0.7.1
|
github.com/CycloneDX/cyclonedx-go v0.7.1
|
||||||
github.com/Masterminds/semver v1.5.0
|
github.com/Masterminds/semver v1.5.0
|
||||||
github.com/Masterminds/sprig/v3 v3.2.3
|
github.com/Masterminds/sprig/v3 v3.2.3
|
||||||
github.com/anchore/go-logger v0.0.0-20220728155337-03b66a5207d8
|
github.com/anchore/bubbly v0.0.0-20230622134437-40226fdcc0f5
|
||||||
|
github.com/anchore/clio v0.0.0-20230602170917-e747e60c4aa0
|
||||||
|
github.com/anchore/go-logger v0.0.0-20230531193951-db5ae83e7dbe
|
||||||
github.com/anchore/stereoscope v0.0.0-20230627195312-cd49355d934e
|
github.com/anchore/stereoscope v0.0.0-20230627195312-cd49355d934e
|
||||||
|
github.com/charmbracelet/bubbletea v0.24.2
|
||||||
|
github.com/charmbracelet/lipgloss v0.7.1
|
||||||
github.com/dave/jennifer v1.6.1
|
github.com/dave/jennifer v1.6.1
|
||||||
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
|
github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da
|
||||||
github.com/docker/docker v24.0.2+incompatible
|
github.com/docker/docker v24.0.2+incompatible
|
||||||
github.com/github/go-spdx/v2 v2.1.2
|
github.com/github/go-spdx/v2 v2.1.2
|
||||||
|
github.com/gkampitakis/go-snaps v0.4.0
|
||||||
github.com/go-git/go-billy/v5 v5.4.1
|
github.com/go-git/go-billy/v5 v5.4.1
|
||||||
github.com/go-git/go-git/v5 v5.7.0
|
github.com/go-git/go-git/v5 v5.7.0
|
||||||
github.com/google/go-containerregistry v0.15.2
|
github.com/google/go-containerregistry v0.15.2
|
||||||
|
@ -67,6 +72,7 @@ require (
|
||||||
github.com/opencontainers/go-digest v1.0.0
|
github.com/opencontainers/go-digest v1.0.0
|
||||||
github.com/sassoftware/go-rpmutils v0.2.0
|
github.com/sassoftware/go-rpmutils v0.2.0
|
||||||
github.com/vbatts/go-mtree v0.5.3
|
github.com/vbatts/go-mtree v0.5.3
|
||||||
|
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1
|
||||||
golang.org/x/exp v0.0.0-20230202163644-54bba9f4231b
|
golang.org/x/exp v0.0.0-20230202163644-54bba9f4231b
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
modernc.org/sqlite v1.23.1
|
modernc.org/sqlite v1.23.1
|
||||||
|
@ -80,9 +86,14 @@ require (
|
||||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect
|
github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect
|
||||||
github.com/acomagu/bufpipe v1.0.4 // indirect
|
github.com/acomagu/bufpipe v1.0.4 // indirect
|
||||||
|
github.com/anchore/fangs v0.0.0-20230531202914-48a718c6b4ba // indirect
|
||||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
|
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/becheran/wildmatch-go v1.0.0 // indirect
|
github.com/becheran/wildmatch-go v1.0.0 // indirect
|
||||||
|
github.com/charmbracelet/bubbles v0.16.1 // indirect
|
||||||
|
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||||
github.com/cloudflare/circl v1.3.3 // indirect
|
github.com/cloudflare/circl v1.3.3 // indirect
|
||||||
|
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
||||||
github.com/containerd/containerd v1.7.0 // indirect
|
github.com/containerd/containerd v1.7.0 // indirect
|
||||||
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
|
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
@ -93,14 +104,18 @@ require (
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
|
github.com/felixge/fgprof v0.9.3 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.0 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.0 // indirect
|
||||||
|
github.com/gkampitakis/ciinfo v0.1.1 // indirect
|
||||||
|
github.com/gkampitakis/go-diff v1.3.0 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-restruct/restruct v1.2.0-alpha // indirect
|
github.com/go-restruct/restruct v1.2.0-alpha // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/huandu/xstrings v1.3.3 // indirect
|
github.com/huandu/xstrings v1.3.3 // indirect
|
||||||
|
@ -112,21 +127,32 @@ require (
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
github.com/klauspost/compress v1.16.5 // indirect
|
github.com/klauspost/compress v1.16.5 // indirect
|
||||||
github.com/klauspost/pgzip v1.2.5 // indirect
|
github.com/klauspost/pgzip v1.2.5 // indirect
|
||||||
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 // indirect
|
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
github.com/mattn/go-isatty v0.0.18 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/reflow v0.3.0 // indirect
|
||||||
|
github.com/muesli/termenv v0.15.1 // indirect
|
||||||
github.com/nwaples/rardecode v1.1.0 // indirect
|
github.com/nwaples/rardecode v1.1.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.0-rc3 // indirect
|
github.com/opencontainers/image-spec v1.1.0-rc3 // indirect
|
||||||
|
github.com/pborman/indent v1.2.1 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.15 // indirect
|
github.com/pierrec/lz4/v4 v4.1.15 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
|
github.com/pkg/profile v1.7.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 // indirect
|
||||||
github.com/shopspring/decimal v1.2.0 // indirect
|
github.com/shopspring/decimal v1.2.0 // indirect
|
||||||
github.com/skeema/knownhosts v1.1.1 // indirect
|
github.com/skeema/knownhosts v1.1.1 // indirect
|
||||||
github.com/spf13/cast v1.5.1 // indirect
|
github.com/spf13/cast v1.5.1 // indirect
|
||||||
|
|
71
go.sum
71
go.sum
|
@ -86,8 +86,14 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/anchore/go-logger v0.0.0-20220728155337-03b66a5207d8 h1:imgMA0gN0TZx7PSa/pdWqXadBvrz8WsN6zySzCe4XX0=
|
github.com/anchore/bubbly v0.0.0-20230622134437-40226fdcc0f5 h1:ylXHybVevy9Musod3gplxsn7g9Ws7ET/XcCrWFXkuvw=
|
||||||
github.com/anchore/go-logger v0.0.0-20220728155337-03b66a5207d8/go.mod h1:+gPap4jha079qzRTUaehv+UZ6sSdaNwkH0D3b6zhTuk=
|
github.com/anchore/bubbly v0.0.0-20230622134437-40226fdcc0f5/go.mod h1:tBC1jAU9gk7ekAbUmBXCuRX1l5Z9sMSqgcGSgsV1ECY=
|
||||||
|
github.com/anchore/clio v0.0.0-20230602170917-e747e60c4aa0 h1:g0UqRW60JDrf5fb40RUyIwwcfQ3nAJqGj4aUCVTwFE4=
|
||||||
|
github.com/anchore/clio v0.0.0-20230602170917-e747e60c4aa0/go.mod h1:0IQVIROfgRX4WZFMfgsbNZmMgLKqW/KgByyJDYvWiDE=
|
||||||
|
github.com/anchore/fangs v0.0.0-20230531202914-48a718c6b4ba h1:tJ186HK8e0Lf+hhNWX4fJrq14yj3mw8JQkkLhA0nFhE=
|
||||||
|
github.com/anchore/fangs v0.0.0-20230531202914-48a718c6b4ba/go.mod h1:E3zNHEz7mizIFGJhuX+Ga7AbCmEN5TfzVDxmOfj7XZw=
|
||||||
|
github.com/anchore/go-logger v0.0.0-20230531193951-db5ae83e7dbe h1:Df867YMmymdMG6z5IW8pR0/2CRpLIjYnaTXLp6j+s0k=
|
||||||
|
github.com/anchore/go-logger v0.0.0-20230531193951-db5ae83e7dbe/go.mod h1:ubLFmlsv8/DFUQrZwY5syT5/8Er3ugSr4rDFwHsE3hg=
|
||||||
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU=
|
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU=
|
||||||
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb/go.mod h1:DmTY2Mfcv38hsHbG78xMiTDdxFtkHpgYNVDPsF2TgHk=
|
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb/go.mod h1:DmTY2Mfcv38hsHbG78xMiTDdxFtkHpgYNVDPsF2TgHk=
|
||||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc=
|
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc=
|
||||||
|
@ -112,6 +118,8 @@ github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb
|
||||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA=
|
github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA=
|
||||||
github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4=
|
github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
|
@ -127,6 +135,14 @@ github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA
|
||||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY=
|
||||||
|
github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc=
|
||||||
|
github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY=
|
||||||
|
github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg=
|
||||||
|
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
||||||
|
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||||
|
github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
|
||||||
|
github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
|
@ -146,6 +162,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
|
||||||
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||||
|
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
|
||||||
|
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
|
||||||
github.com/containerd/containerd v1.7.0 h1:G/ZQr3gMZs6ZT0qPUZ15znx5QSdQdASW11nXTLTM2Pg=
|
github.com/containerd/containerd v1.7.0 h1:G/ZQr3gMZs6ZT0qPUZ15znx5QSdQdASW11nXTLTM2Pg=
|
||||||
github.com/containerd/containerd v1.7.0/go.mod h1:QfR7Efgb/6X2BDpTPJRvPTYDE9rsF0FsXX9J8sIs/sc=
|
github.com/containerd/containerd v1.7.0/go.mod h1:QfR7Efgb/6X2BDpTPJRvPTYDE9rsF0FsXX9J8sIs/sc=
|
||||||
github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
|
github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
|
||||||
|
@ -202,6 +220,8 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL
|
||||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||||
github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=
|
github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=
|
||||||
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
|
github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=
|
||||||
|
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||||
|
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||||
|
@ -211,6 +231,12 @@ github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmx
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/github/go-spdx/v2 v2.1.2 h1:p+Tv0yMgcuO0/vnMe9Qh4tmUgYhI6AsLVlakZ/Sx+DM=
|
github.com/github/go-spdx/v2 v2.1.2 h1:p+Tv0yMgcuO0/vnMe9Qh4tmUgYhI6AsLVlakZ/Sx+DM=
|
||||||
github.com/github/go-spdx/v2 v2.1.2/go.mod h1:hMCrsFgT0QnCwn7G8gxy/MxMpy67WgZrwFeISTn0o6w=
|
github.com/github/go-spdx/v2 v2.1.2/go.mod h1:hMCrsFgT0QnCwn7G8gxy/MxMpy67WgZrwFeISTn0o6w=
|
||||||
|
github.com/gkampitakis/ciinfo v0.1.1 h1:dz1LCkOd+zmZ3YYlFNpr0hRDqGY7Ox2mcaltHzdahqk=
|
||||||
|
github.com/gkampitakis/ciinfo v0.1.1/go.mod h1:bVaOGziPqf8PoeYZxatq1HmCsJUmv191hLnFboYxd9Y=
|
||||||
|
github.com/gkampitakis/go-diff v1.3.0 h1:Szdbo5w73LSQ9sQ02h+NSSf2ZlW/E8naJCI1ZzQtWgE=
|
||||||
|
github.com/gkampitakis/go-diff v1.3.0/go.mod h1:QUJDQRA0JkEX0d7tgDaBHzJv9IH6k6e91TByC+9/RFk=
|
||||||
|
github.com/gkampitakis/go-snaps v0.4.0 h1:yTMQ4RaGrQvsr70XZRoxZeJiMkmdLbZ9fWpW/vypdVk=
|
||||||
|
github.com/gkampitakis/go-snaps v0.4.0/go.mod h1:xYclGIA7Al0CoYwehW0dd/NEr6oJge+1Dl4OWWxQUWY=
|
||||||
github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4=
|
github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4=
|
||||||
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
|
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
|
@ -315,7 +341,9 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe
|
||||||
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
|
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||||
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
@ -372,6 +400,7 @@ github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0
|
||||||
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
|
github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||||
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
|
github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
|
||||||
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
|
||||||
|
@ -414,13 +443,17 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
|
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
|
||||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
|
github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
|
||||||
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||||
|
@ -442,11 +475,14 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
|
||||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
|
||||||
|
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||||
|
@ -482,6 +518,14 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
|
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA=
|
||||||
|
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
||||||
|
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
||||||
|
github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs=
|
||||||
|
github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
|
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
|
||||||
|
@ -494,6 +538,8 @@ github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0
|
||||||
github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
|
github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
|
||||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
|
github.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM=
|
||||||
|
github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrpz5K6Vw=
|
||||||
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||||
|
@ -504,10 +550,13 @@ github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0
|
||||||
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
|
||||||
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||||
|
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
||||||
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
@ -529,11 +578,14 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||||
github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
|
github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
|
||||||
|
@ -618,8 +670,8 @@ github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RV
|
||||||
github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY=
|
github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY=
|
||||||
github.com/vifraa/gopom v0.2.1 h1:MYVMAMyiGzXPPy10EwojzKIL670kl5Zbae+o3fFvQEM=
|
github.com/vifraa/gopom v0.2.1 h1:MYVMAMyiGzXPPy10EwojzKIL670kl5Zbae+o3fFvQEM=
|
||||||
github.com/vifraa/gopom v0.2.1/go.mod h1:oPa1dcrGrtlO37WPDBm5SqHAT+wTgF8An1Q71Z6Vv4o=
|
github.com/vifraa/gopom v0.2.1/go.mod h1:oPa1dcrGrtlO37WPDBm5SqHAT+wTgF8An1Q71Z6Vv4o=
|
||||||
github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5 h1:phTLPgMRDYTizrBSKsNSOa2zthoC2KsJsaY/8sg3rD8=
|
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 h1:jIVmlAFIqV3d+DOxazTR9v+zgj8+VYuQBzPgBZvWBHA=
|
||||||
github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5/go.mod h1:JPirS5jde/CF5qIjcK4WX+eQmKXdPc6vcZkJ/P0hfPw=
|
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651/go.mod h1:b26F2tHLqaoRQf8DywqzVaV1MQ9yvjb0OMcNl7Nxu20=
|
||||||
github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5 h1:lwgTsTy18nYqASnH58qyfRW/ldj7Gt2zzBvgYPzdA4s=
|
github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5 h1:lwgTsTy18nYqASnH58qyfRW/ldj7Gt2zzBvgYPzdA4s=
|
||||||
github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA=
|
github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA=
|
||||||
github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb h1:Yz6VVOcLuWLAHYlJzTw7JKnWxdV/WXpug2X0quEzRnY=
|
github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb h1:Yz6VVOcLuWLAHYlJzTw7JKnWxdV/WXpug2X0quEzRnY=
|
||||||
|
@ -643,6 +695,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1 h1:V+UsotZpAVvfj3X/LMoEytoLzSiP6Lg0F7wdVyu9gGg=
|
||||||
|
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1/go.mod h1:ly2RBz4mnz1yeuVbQA/VFwGjK3mnHGRj1JuoG336Bis=
|
||||||
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
||||||
go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
||||||
go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs=
|
go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs=
|
||||||
|
@ -878,6 +932,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|
|
@ -16,20 +16,20 @@ package bus
|
||||||
import "github.com/wagoodman/go-partybus"
|
import "github.com/wagoodman/go-partybus"
|
||||||
|
|
||||||
var publisher partybus.Publisher
|
var publisher partybus.Publisher
|
||||||
var active bool
|
|
||||||
|
|
||||||
// SetPublisher sets the singleton event bus publisher. This is optional; if no bus is provided, the library will
|
// Set 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.
|
// behave no differently than if a bus had been provided.
|
||||||
func SetPublisher(p partybus.Publisher) {
|
func Set(p partybus.Publisher) {
|
||||||
publisher = p
|
publisher = p
|
||||||
if p != nil {
|
}
|
||||||
active = true
|
|
||||||
}
|
func Get() partybus.Publisher {
|
||||||
|
return publisher
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish an event onto the bus. If there is no bus set by the calling application, this does nothing.
|
// 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(e partybus.Event) {
|
||||||
if active {
|
if publisher != nil {
|
||||||
publisher.Publish(event)
|
publisher.Publish(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
32
internal/bus/helpers.go
Normal file
32
internal/bus/helpers.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package bus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft/event"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Exit() {
|
||||||
|
Publish(partybus.Event{
|
||||||
|
Type: event.CLIExit,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Report(report string) {
|
||||||
|
if len(report) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
report = log.Redactor.RedactString(report)
|
||||||
|
Publish(partybus.Event{
|
||||||
|
Type: event.CLIReport,
|
||||||
|
Value: report,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Notify(message string) {
|
||||||
|
Publish(partybus.Event{
|
||||||
|
Type: event.CLINotification,
|
||||||
|
Value: message,
|
||||||
|
})
|
||||||
|
}
|
|
@ -6,67 +6,86 @@ package log
|
||||||
import (
|
import (
|
||||||
"github.com/anchore/go-logger"
|
"github.com/anchore/go-logger"
|
||||||
"github.com/anchore/go-logger/adapter/discard"
|
"github.com/anchore/go-logger/adapter/discard"
|
||||||
|
"github.com/anchore/go-logger/adapter/redact"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Log is the singleton used to facilitate logging internally within syft
|
var (
|
||||||
var Log logger.Logger = discard.New()
|
// log is the singleton used to facilitate logging internally within
|
||||||
|
log = discard.New()
|
||||||
|
|
||||||
|
store = redact.NewStore()
|
||||||
|
|
||||||
|
Redactor = store.(redact.Redactor)
|
||||||
|
)
|
||||||
|
|
||||||
|
func Set(l logger.Logger) {
|
||||||
|
log = redact.New(l, store)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get() logger.Logger {
|
||||||
|
return log
|
||||||
|
}
|
||||||
|
|
||||||
|
func Redact(values ...string) {
|
||||||
|
store.Add(values...)
|
||||||
|
}
|
||||||
|
|
||||||
// Errorf takes a formatted template string and template arguments for the error logging level.
|
// Errorf takes a formatted template string and template arguments for the error logging level.
|
||||||
func Errorf(format string, args ...interface{}) {
|
func Errorf(format string, args ...interface{}) {
|
||||||
Log.Errorf(format, args...)
|
log.Errorf(format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error logs the given arguments at the error logging level.
|
// Error logs the given arguments at the error logging level.
|
||||||
func Error(args ...interface{}) {
|
func Error(args ...interface{}) {
|
||||||
Log.Error(args...)
|
log.Error(args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warnf takes a formatted template string and template arguments for the warning logging level.
|
// Warnf takes a formatted template string and template arguments for the warning logging level.
|
||||||
func Warnf(format string, args ...interface{}) {
|
func Warnf(format string, args ...interface{}) {
|
||||||
Log.Warnf(format, args...)
|
log.Warnf(format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn logs the given arguments at the warning logging level.
|
// Warn logs the given arguments at the warning logging level.
|
||||||
func Warn(args ...interface{}) {
|
func Warn(args ...interface{}) {
|
||||||
Log.Warn(args...)
|
log.Warn(args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Infof takes a formatted template string and template arguments for the info logging level.
|
// Infof takes a formatted template string and template arguments for the info logging level.
|
||||||
func Infof(format string, args ...interface{}) {
|
func Infof(format string, args ...interface{}) {
|
||||||
Log.Infof(format, args...)
|
log.Infof(format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info logs the given arguments at the info logging level.
|
// Info logs the given arguments at the info logging level.
|
||||||
func Info(args ...interface{}) {
|
func Info(args ...interface{}) {
|
||||||
Log.Info(args...)
|
log.Info(args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debugf takes a formatted template string and template arguments for the debug logging level.
|
// Debugf takes a formatted template string and template arguments for the debug logging level.
|
||||||
func Debugf(format string, args ...interface{}) {
|
func Debugf(format string, args ...interface{}) {
|
||||||
Log.Debugf(format, args...)
|
log.Debugf(format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug logs the given arguments at the debug logging level.
|
// Debug logs the given arguments at the debug logging level.
|
||||||
func Debug(args ...interface{}) {
|
func Debug(args ...interface{}) {
|
||||||
Log.Debug(args...)
|
log.Debug(args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tracef takes a formatted template string and template arguments for the trace logging level.
|
// Tracef takes a formatted template string and template arguments for the trace logging level.
|
||||||
func Tracef(format string, args ...interface{}) {
|
func Tracef(format string, args ...interface{}) {
|
||||||
Log.Tracef(format, args...)
|
log.Tracef(format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trace logs the given arguments at the trace logging level.
|
// Trace logs the given arguments at the trace logging level.
|
||||||
func Trace(args ...interface{}) {
|
func Trace(args ...interface{}) {
|
||||||
Log.Trace(args...)
|
log.Trace(args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithFields returns a message logger with multiple key-value fields.
|
// WithFields returns a message logger with multiple key-value fields.
|
||||||
func WithFields(fields ...interface{}) logger.MessageLogger {
|
func WithFields(fields ...interface{}) logger.MessageLogger {
|
||||||
return Log.WithFields(fields...)
|
return log.WithFields(fields...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nested returns a new logger with hard coded key-value pairs
|
// Nested returns a new logger with hard coded key-value pairs
|
||||||
func Nested(fields ...interface{}) logger.Logger {
|
func Nested(fields ...interface{}) logger.Logger {
|
||||||
return Log.Nested(fields...)
|
return log.Nested(fields...)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/wagoodman/go-partybus"
|
|
||||||
|
|
||||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
|
||||||
)
|
|
||||||
|
|
||||||
// handleExit is a UI function for processing the Exit bus event,
|
|
||||||
// and calling the given function to output the contents.
|
|
||||||
func handleExit(event partybus.Event) error {
|
|
||||||
// show the report to stdout
|
|
||||||
fn, err := syftEventParsers.ParseExit(event)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("bad CatalogerFinished event: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := fn(); err != nil {
|
|
||||||
return fmt.Errorf("unable to show package catalog report: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
package components
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: move me to a common module (used in multiple repos)
|
|
||||||
|
|
||||||
const (
|
|
||||||
SpinnerDotSet = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Spinner struct {
|
|
||||||
index int
|
|
||||||
charset []string
|
|
||||||
lock sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSpinner(charset string) Spinner {
|
|
||||||
return Spinner{
|
|
||||||
charset: strings.Split(charset, ""),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Spinner) Current() string {
|
|
||||||
s.lock.Lock()
|
|
||||||
defer s.lock.Unlock()
|
|
||||||
|
|
||||||
return s.charset[s.index]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Spinner) Next() string {
|
|
||||||
s.lock.Lock()
|
|
||||||
defer s.lock.Unlock()
|
|
||||||
c := s.charset[s.index]
|
|
||||||
s.index++
|
|
||||||
if s.index >= len(s.charset) {
|
|
||||||
s.index = 0
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
|
@ -1,154 +0,0 @@
|
||||||
//go:build linux || darwin || netbsd
|
|
||||||
// +build linux darwin netbsd
|
|
||||||
|
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/wagoodman/go-partybus"
|
|
||||||
"github.com/wagoodman/jotframe/pkg/frame"
|
|
||||||
|
|
||||||
"github.com/anchore/go-logger"
|
|
||||||
"github.com/anchore/syft/internal/log"
|
|
||||||
syftEvent "github.com/anchore/syft/syft/event"
|
|
||||||
"github.com/anchore/syft/ui"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ephemeralTerminalUI 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)
|
|
||||||
type ephemeralTerminalUI struct {
|
|
||||||
unsubscribe func() error
|
|
||||||
handler *ui.Handler
|
|
||||||
waitGroup *sync.WaitGroup
|
|
||||||
frame *frame.Frame
|
|
||||||
logBuffer *bytes.Buffer
|
|
||||||
uiOutput *os.File
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEphemeralTerminalUI writes all events to a TUI and writes the final report to the given writer.
|
|
||||||
func NewEphemeralTerminalUI() UI {
|
|
||||||
return &ephemeralTerminalUI{
|
|
||||||
handler: ui.NewHandler(),
|
|
||||||
waitGroup: &sync.WaitGroup{},
|
|
||||||
uiOutput: os.Stderr,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ephemeralTerminalUI) Setup(unsubscribe func() error) error {
|
|
||||||
h.unsubscribe = unsubscribe
|
|
||||||
hideCursor(h.uiOutput)
|
|
||||||
|
|
||||||
// prep the logger to not clobber the screen from now on (logrus only)
|
|
||||||
h.logBuffer = bytes.NewBufferString("")
|
|
||||||
logController, ok := log.Log.(logger.Controller)
|
|
||||||
if ok {
|
|
||||||
logController.SetOutput(h.logBuffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.openScreen()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ephemeralTerminalUI) Handle(event partybus.Event) error {
|
|
||||||
ctx := context.Background()
|
|
||||||
switch {
|
|
||||||
case h.handler.RespondsTo(event):
|
|
||||||
if err := h.handler.Handle(ctx, h.frame, event, h.waitGroup); err != nil {
|
|
||||||
log.Errorf("unable to show %s event: %+v", event.Type, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
case event.Type == syftEvent.AppUpdateAvailable:
|
|
||||||
if err := handleAppUpdateAvailable(ctx, h.frame, event, h.waitGroup); err != nil {
|
|
||||||
log.Errorf("unable to show %s event: %+v", event.Type, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
case event.Type == syftEvent.Exit:
|
|
||||||
// we need to close the screen now since signaling the sbom is ready means that we
|
|
||||||
// are about to write bytes to stdout, so we should reset the terminal state first
|
|
||||||
h.closeScreen(false)
|
|
||||||
|
|
||||||
if err := handleExit(event); err != nil {
|
|
||||||
log.Errorf("unable to show %s event: %+v", event.Type, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is the last expected event, stop listening to events
|
|
||||||
return h.unsubscribe()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ephemeralTerminalUI) openScreen() error {
|
|
||||||
config := frame.Config{
|
|
||||||
PositionPolicy: frame.PolicyFloatForward,
|
|
||||||
// only report output to stderr, reserve report output for stdout
|
|
||||||
Output: h.uiOutput,
|
|
||||||
}
|
|
||||||
|
|
||||||
fr, err := frame.New(config)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create the screen object: %w", err)
|
|
||||||
}
|
|
||||||
h.frame = fr
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ephemeralTerminalUI) closeScreen(force bool) {
|
|
||||||
// we may have other background processes still displaying progress, wait for them to
|
|
||||||
// finish before discontinuing dynamic content and showing the final report
|
|
||||||
if !h.frame.IsClosed() {
|
|
||||||
if !force {
|
|
||||||
h.waitGroup.Wait()
|
|
||||||
}
|
|
||||||
h.frame.Close()
|
|
||||||
// TODO: there is a race condition within frame.Close() that sometimes leads to an extra blank line being output
|
|
||||||
frame.Close()
|
|
||||||
|
|
||||||
// only flush the log on close
|
|
||||||
h.flushLog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ephemeralTerminalUI) flushLog() {
|
|
||||||
// flush any errors to the screen before the report
|
|
||||||
logController, ok := log.Log.(logger.Controller)
|
|
||||||
if ok {
|
|
||||||
fmt.Fprint(logController.GetOutput(), h.logBuffer.String())
|
|
||||||
logController.SetOutput(h.uiOutput)
|
|
||||||
} else {
|
|
||||||
fmt.Fprint(h.uiOutput, h.logBuffer.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ephemeralTerminalUI) Teardown(force bool) error {
|
|
||||||
h.closeScreen(force)
|
|
||||||
showCursor(h.uiOutput)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func hideCursor(output io.Writer) {
|
|
||||||
fmt.Fprint(output, "\x1b[?25l")
|
|
||||||
}
|
|
||||||
|
|
||||||
func showCursor(output io.Writer) {
|
|
||||||
fmt.Fprint(output, "\x1b[?25h")
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
//go:build linux || darwin || netbsd
|
|
||||||
// +build linux darwin netbsd
|
|
||||||
|
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/gookit/color"
|
|
||||||
"github.com/wagoodman/go-partybus"
|
|
||||||
"github.com/wagoodman/jotframe/pkg/frame"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal"
|
|
||||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
|
||||||
)
|
|
||||||
|
|
||||||
// handleAppUpdateAvailable is a UI handler function to display a new application version to the top of the screen.
|
|
||||||
func handleAppUpdateAvailable(_ context.Context, fr *frame.Frame, event partybus.Event, _ *sync.WaitGroup) error {
|
|
||||||
newVersion, err := syftEventParsers.ParseAppUpdateAvailable(event)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("bad AppUpdateAvailable event: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
line, err := fr.Prepend()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
message := color.Magenta.Sprintf("New version of %s is available: %s", internal.ApplicationName, newVersion)
|
|
||||||
_, _ = io.WriteString(line, message)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/wagoodman/go-partybus"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/log"
|
|
||||||
syftEvent "github.com/anchore/syft/syft/event"
|
|
||||||
)
|
|
||||||
|
|
||||||
type loggerUI struct {
|
|
||||||
unsubscribe func() error
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLoggerUI writes all events to the common application logger and writes the final report to the given writer.
|
|
||||||
func NewLoggerUI() UI {
|
|
||||||
return &loggerUI{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *loggerUI) Setup(unsubscribe func() error) error {
|
|
||||||
l.unsubscribe = unsubscribe
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l loggerUI) Handle(event partybus.Event) error {
|
|
||||||
// ignore all events except for the final event
|
|
||||||
if event.Type != syftEvent.Exit {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := handleExit(event); err != nil {
|
|
||||||
log.Warnf("unable to show catalog image finished event: %+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is the last expected event, stop listening to events
|
|
||||||
return l.unsubscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l loggerUI) Teardown(_ bool) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/wagoodman/go-partybus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UI interface {
|
|
||||||
Setup(unsubscribe func() error) error
|
|
||||||
partybus.Handler
|
|
||||||
Teardown(force bool) error
|
|
||||||
}
|
|
|
@ -4,37 +4,51 @@ defined here there should be a corresponding event parser defined in the parsers
|
||||||
*/
|
*/
|
||||||
package event
|
package event
|
||||||
|
|
||||||
import "github.com/wagoodman/go-partybus"
|
import (
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// AppUpdateAvailable is a partybus event that occurs when an application update is available
|
typePrefix = internal.ApplicationName
|
||||||
AppUpdateAvailable partybus.EventType = "syft-app-update-available"
|
cliTypePrefix = typePrefix + "-cli"
|
||||||
|
|
||||||
|
// Events from the syft library
|
||||||
|
|
||||||
// PackageCatalogerStarted is a partybus event that occurs when the package cataloging has begun
|
// PackageCatalogerStarted is a partybus event that occurs when the package cataloging has begun
|
||||||
PackageCatalogerStarted partybus.EventType = "syft-package-cataloger-started-event"
|
PackageCatalogerStarted partybus.EventType = typePrefix + "-package-cataloger-started-event"
|
||||||
|
|
||||||
//nolint:gosec
|
//nolint:gosec
|
||||||
// SecretsCatalogerStarted is a partybus event that occurs when the secrets cataloging has begun
|
// SecretsCatalogerStarted is a partybus event that occurs when the secrets cataloging has begun
|
||||||
SecretsCatalogerStarted partybus.EventType = "syft-secrets-cataloger-started-event"
|
SecretsCatalogerStarted partybus.EventType = typePrefix + "-secrets-cataloger-started-event"
|
||||||
|
|
||||||
// FileMetadataCatalogerStarted is a partybus event that occurs when the file metadata cataloging has begun
|
// FileMetadataCatalogerStarted is a partybus event that occurs when the file metadata cataloging has begun
|
||||||
FileMetadataCatalogerStarted partybus.EventType = "syft-file-metadata-cataloger-started-event"
|
FileMetadataCatalogerStarted partybus.EventType = typePrefix + "-file-metadata-cataloger-started-event"
|
||||||
|
|
||||||
// FileDigestsCatalogerStarted is a partybus event that occurs when the file digests cataloging has begun
|
// FileDigestsCatalogerStarted is a partybus event that occurs when the file digests cataloging has begun
|
||||||
FileDigestsCatalogerStarted partybus.EventType = "syft-file-digests-cataloger-started-event"
|
FileDigestsCatalogerStarted partybus.EventType = typePrefix + "-file-digests-cataloger-started-event"
|
||||||
|
|
||||||
// FileIndexingStarted is a partybus event that occurs when the directory resolver begins indexing a filesystem
|
// FileIndexingStarted is a partybus event that occurs when the directory resolver begins indexing a filesystem
|
||||||
FileIndexingStarted partybus.EventType = "syft-file-indexing-started-event"
|
FileIndexingStarted partybus.EventType = typePrefix + "-file-indexing-started-event"
|
||||||
|
|
||||||
// Exit is a partybus event that occurs when an analysis result is ready for final presentation
|
|
||||||
Exit partybus.EventType = "syft-exit-event"
|
|
||||||
|
|
||||||
// ImportStarted is a partybus event that occurs when an SBOM upload process has begun
|
|
||||||
ImportStarted partybus.EventType = "syft-import-started-event"
|
|
||||||
|
|
||||||
// AttestationStarted is a partybus event that occurs when starting an SBOM attestation process
|
// AttestationStarted is a partybus event that occurs when starting an SBOM attestation process
|
||||||
AttestationStarted partybus.EventType = "syft-attestation-started-event"
|
AttestationStarted partybus.EventType = typePrefix + "-attestation-started-event"
|
||||||
|
|
||||||
// CatalogerTaskStarted is a partybus event that occurs when starting a task within a cataloger
|
// CatalogerTaskStarted is a partybus event that occurs when starting a task within a cataloger
|
||||||
CatalogerTaskStarted partybus.EventType = "syft-cataloger-task-started"
|
CatalogerTaskStarted partybus.EventType = typePrefix + "-cataloger-task-started"
|
||||||
|
|
||||||
|
// Events exclusively for the CLI
|
||||||
|
|
||||||
|
// CLIAppUpdateAvailable is a partybus event that occurs when an application update is available
|
||||||
|
CLIAppUpdateAvailable partybus.EventType = cliTypePrefix + "-app-update-available"
|
||||||
|
|
||||||
|
// CLIReport is a partybus event that occurs when an analysis result is ready for final presentation to stdout
|
||||||
|
CLIReport partybus.EventType = cliTypePrefix + "-report"
|
||||||
|
|
||||||
|
// CLINotification is a partybus event that occurs when auxiliary information is ready for presentation to stderr
|
||||||
|
CLINotification partybus.EventType = cliTypePrefix + "-notification"
|
||||||
|
|
||||||
|
// CLIExit is a partybus event that occurs when an analysis result is ready for final presentation
|
||||||
|
CLIExit partybus.EventType = cliTypePrefix + "-exit-event"
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
package event
|
package monitor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/wagoodman/go-partybus"
|
"github.com/wagoodman/go-partybus"
|
||||||
"github.com/wagoodman/go-progress"
|
"github.com/wagoodman/go-progress"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/bus"
|
"github.com/anchore/syft/internal/bus"
|
||||||
|
"github.com/anchore/syft/syft/event"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: this should be refactored to support read-only/write-only access using idioms of the progress lib
|
||||||
|
|
||||||
type CatalogerTask struct {
|
type CatalogerTask struct {
|
||||||
prog *progress.Manual
|
prog *progress.Manual
|
||||||
// Title
|
// Title
|
||||||
|
@ -25,7 +28,7 @@ func (e *CatalogerTask) init() {
|
||||||
e.prog = progress.NewManual(-1)
|
e.prog = progress.NewManual(-1)
|
||||||
|
|
||||||
bus.Publish(partybus.Event{
|
bus.Publish(partybus.Event{
|
||||||
Type: CatalogerTaskStarted,
|
Type: event.CatalogerTaskStarted,
|
||||||
Source: e,
|
Source: e,
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
type ShellProgress struct {
|
type ShellProgress struct {
|
||||||
io.Reader
|
io.Reader
|
||||||
*progress.Manual
|
progress.Progressable
|
||||||
}
|
}
|
||||||
|
|
||||||
type Title struct {
|
type Title struct {
|
||||||
|
|
|
@ -23,7 +23,7 @@ type ErrBadPayload struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ErrBadPayload) Error() string {
|
func (e *ErrBadPayload) Error() string {
|
||||||
return fmt.Sprintf("event='%s' has bad event payload field='%v': '%+v'", string(e.Type), e.Field, e.Value)
|
return fmt.Sprintf("event='%s' has bad event payload field=%q: %q", string(e.Type), e.Field, e.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPayloadErr(t partybus.EventType, field string, value interface{}) error {
|
func newPayloadErr(t partybus.EventType, field string, value interface{}) error {
|
||||||
|
@ -111,12 +111,12 @@ func ParseFileIndexingStarted(e partybus.Event) (string, progress.StagedProgress
|
||||||
return path, prog, nil
|
return path, prog, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseCatalogerTaskStarted(e partybus.Event) (*event.CatalogerTask, error) {
|
func ParseCatalogerTaskStarted(e partybus.Event) (*monitor.CatalogerTask, error) {
|
||||||
if err := checkEventType(e.Type, event.CatalogerTaskStarted); err != nil {
|
if err := checkEventType(e.Type, event.CatalogerTaskStarted); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
source, ok := e.Source.(*event.CatalogerTask)
|
source, ok := e.Source.(*monitor.CatalogerTask)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, newPayloadErr(e.Type, "Source", e.Source)
|
return nil, newPayloadErr(e.Type, "Source", e.Source)
|
||||||
}
|
}
|
||||||
|
@ -124,50 +124,6 @@ func ParseCatalogerTaskStarted(e partybus.Event) (*event.CatalogerTask, error) {
|
||||||
return source, nil
|
return source, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseExit(e partybus.Event) (func() error, error) {
|
|
||||||
if err := checkEventType(e.Type, event.Exit); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
fn, ok := e.Value.(func() error)
|
|
||||||
if !ok {
|
|
||||||
return nil, newPayloadErr(e.Type, "Value", e.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseAppUpdateAvailable(e partybus.Event) (string, error) {
|
|
||||||
if err := checkEventType(e.Type, event.AppUpdateAvailable); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
newVersion, ok := e.Value.(string)
|
|
||||||
if !ok {
|
|
||||||
return "", newPayloadErr(e.Type, "Value", e.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return newVersion, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseImportStarted(e partybus.Event) (string, progress.StagedProgressable, error) {
|
|
||||||
if err := checkEventType(e.Type, event.ImportStarted); err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
host, ok := e.Source.(string)
|
|
||||||
if !ok {
|
|
||||||
return "", nil, newPayloadErr(e.Type, "Source", e.Source)
|
|
||||||
}
|
|
||||||
|
|
||||||
prog, ok := e.Value.(progress.StagedProgressable)
|
|
||||||
if !ok {
|
|
||||||
return "", nil, newPayloadErr(e.Type, "Value", e.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return host, prog, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseAttestationStartedEvent(e partybus.Event) (io.Reader, progress.Progressable, *monitor.GenericTask, error) {
|
func ParseAttestationStartedEvent(e partybus.Event) (io.Reader, progress.Progressable, *monitor.GenericTask, error) {
|
||||||
if err := checkEventType(e.Type, event.AttestationStarted); err != nil {
|
if err := checkEventType(e.Type, event.AttestationStarted); err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
|
@ -183,5 +139,71 @@ func ParseAttestationStartedEvent(e partybus.Event) (io.Reader, progress.Progres
|
||||||
return nil, nil, nil, newPayloadErr(e.Type, "Value", e.Value)
|
return nil, nil, nil, newPayloadErr(e.Type, "Value", e.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sp.Reader, sp.Manual, &source, nil
|
return sp.Reader, sp.Progressable, &source, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI event types
|
||||||
|
|
||||||
|
func ParseCLIExit(e partybus.Event) (func() error, error) {
|
||||||
|
if err := checkEventType(e.Type, event.CLIExit); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fn, ok := e.Value.(func() error)
|
||||||
|
if !ok {
|
||||||
|
return nil, newPayloadErr(e.Type, "Value", e.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseCLIAppUpdateAvailable(e partybus.Event) (string, error) {
|
||||||
|
if err := checkEventType(e.Type, event.CLIAppUpdateAvailable); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
newVersion, ok := e.Value.(string)
|
||||||
|
if !ok {
|
||||||
|
return "", newPayloadErr(e.Type, "Value", e.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseCLIReport(e partybus.Event) (string, string, error) {
|
||||||
|
if err := checkEventType(e.Type, event.CLIReport); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
context, ok := e.Source.(string)
|
||||||
|
if !ok {
|
||||||
|
// this is optional
|
||||||
|
context = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
report, ok := e.Value.(string)
|
||||||
|
if !ok {
|
||||||
|
return "", "", newPayloadErr(e.Type, "Value", e.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return context, report, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseCLINotification(e partybus.Event) (string, string, error) {
|
||||||
|
if err := checkEventType(e.Type, event.CLINotification); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
context, ok := e.Source.(string)
|
||||||
|
if !ok {
|
||||||
|
// this is optional
|
||||||
|
context = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
notification, ok := e.Value.(string)
|
||||||
|
if !ok {
|
||||||
|
return "", "", newPayloadErr(e.Type, "Value", e.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return context, notification, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -332,14 +332,15 @@ func (r directoryIndexer) addFileToIndex(p string, info os.FileInfo) error {
|
||||||
func (r directoryIndexer) addSymlinkToIndex(p string, info os.FileInfo) (string, error) {
|
func (r directoryIndexer) addSymlinkToIndex(p string, info os.FileInfo) (string, error) {
|
||||||
linkTarget, err := os.Readlink(p)
|
linkTarget, err := os.Readlink(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if runtime.GOOS == WindowsOS {
|
isOnWindows := windows.HostRunningOnWindows()
|
||||||
p = posixToWindows(p)
|
if isOnWindows {
|
||||||
|
p = windows.FromPosix(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
linkTarget, err = filepath.EvalSymlinks(p)
|
linkTarget, err = filepath.EvalSymlinks(p)
|
||||||
|
|
||||||
if runtime.GOOS == WindowsOS {
|
if isOnWindows {
|
||||||
p = windowsToPosix(p)
|
p = windows.ToPosix(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -94,10 +94,10 @@ func newSourceRelationshipsFromCatalog(src source.Source, c *pkg.Collection) []a
|
||||||
|
|
||||||
// SetLogger sets the logger object used for all syft logging calls.
|
// SetLogger sets the logger object used for all syft logging calls.
|
||||||
func SetLogger(logger logger.Logger) {
|
func SetLogger(logger logger.Logger) {
|
||||||
log.Log = logger
|
log.Set(logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetBus sets the event bus for all syft library bus publish events onto (in-library subscriptions are not allowed).
|
// SetBus sets the event bus for all syft library bus publish events onto (in-library subscriptions are not allowed).
|
||||||
func SetBus(b *partybus.Bus) {
|
func SetBus(b *partybus.Bus) {
|
||||||
bus.SetPublisher(b)
|
bus.Set(b)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ package golang
|
||||||
import (
|
import (
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/anchore/syft/syft/event"
|
"github.com/anchore/syft/syft/event/monitor"
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||||
|
@ -37,7 +37,7 @@ func NewGoModuleBinaryCataloger(opts GoCatalogerOpts) pkg.Cataloger {
|
||||||
}
|
}
|
||||||
|
|
||||||
type progressingCataloger struct {
|
type progressingCataloger struct {
|
||||||
progress *event.CatalogerTask
|
progress *monitor.CatalogerTask
|
||||||
cataloger *generic.Cataloger
|
cataloger *generic.Cataloger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ import (
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/licenses"
|
"github.com/anchore/syft/internal/licenses"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/syft/event"
|
"github.com/anchore/syft/syft/event/monitor"
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/internal/fileresolver"
|
"github.com/anchore/syft/syft/internal/fileresolver"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
@ -30,14 +30,14 @@ import (
|
||||||
type goLicenses struct {
|
type goLicenses struct {
|
||||||
opts GoCatalogerOpts
|
opts GoCatalogerOpts
|
||||||
localModCacheResolver file.WritableResolver
|
localModCacheResolver file.WritableResolver
|
||||||
progress *event.CatalogerTask
|
progress *monitor.CatalogerTask
|
||||||
}
|
}
|
||||||
|
|
||||||
func newGoLicenses(opts GoCatalogerOpts) goLicenses {
|
func newGoLicenses(opts GoCatalogerOpts) goLicenses {
|
||||||
return goLicenses{
|
return goLicenses{
|
||||||
opts: opts,
|
opts: opts,
|
||||||
localModCacheResolver: modCacheResolver(opts.localModCacheDir),
|
localModCacheResolver: modCacheResolver(opts.localModCacheDir),
|
||||||
progress: &event.CatalogerTask{
|
progress: &monitor.CatalogerTask{
|
||||||
SubStatus: true,
|
SubStatus: true,
|
||||||
RemoveOnCompletion: true,
|
RemoveOnCompletion: true,
|
||||||
Title: "Downloading go mod",
|
Title: "Downloading go mod",
|
||||||
|
@ -195,7 +195,7 @@ func processCaps(s string) string {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func getModule(progress *event.CatalogerTask, proxies []string, moduleName, moduleVersion string) (fsys fs.FS, err error) {
|
func getModule(progress *monitor.CatalogerTask, proxies []string, moduleName, moduleVersion string) (fsys fs.FS, err error) {
|
||||||
for _, proxy := range proxies {
|
for _, proxy := range proxies {
|
||||||
u, _ := url.Parse(proxy)
|
u, _ := url.Parse(proxy)
|
||||||
if proxy == "direct" {
|
if proxy == "direct" {
|
||||||
|
@ -217,7 +217,7 @@ func getModule(progress *event.CatalogerTask, proxies []string, moduleName, modu
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func getModuleProxy(progress *event.CatalogerTask, proxy string, moduleName string, moduleVersion string) (out fs.FS, _ error) {
|
func getModuleProxy(progress *monitor.CatalogerTask, proxy string, moduleName string, moduleVersion string) (out fs.FS, _ error) {
|
||||||
u := fmt.Sprintf("%s/%s/@v/%s.zip", proxy, moduleName, moduleVersion)
|
u := fmt.Sprintf("%s/%s/@v/%s.zip", proxy, moduleName, moduleVersion)
|
||||||
progress.SetValue(u)
|
progress.SetValue(u)
|
||||||
// get the module zip
|
// get the module zip
|
||||||
|
@ -265,7 +265,7 @@ func findVersionPath(f fs.FS, dir string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func getModuleRepository(progress *event.CatalogerTask, moduleName string, moduleVersion string) (fs.FS, error) {
|
func getModuleRepository(progress *monitor.CatalogerTask, moduleName string, moduleVersion string) (fs.FS, error) {
|
||||||
repoName := moduleName
|
repoName := moduleName
|
||||||
parts := strings.Split(moduleName, "/")
|
parts := strings.Split(moduleName, "/")
|
||||||
if len(parts) > 2 {
|
if len(parts) > 2 {
|
||||||
|
|
|
@ -49,8 +49,7 @@ func parseAuditBinaryEntry(reader unionreader.UnionReader, filename string) []ru
|
||||||
// binary, we should not show warnings/logs in this case.
|
// binary, we should not show warnings/logs in this case.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Use an Info level log here like golang/scan_bin.go
|
log.Tracef("rust cataloger: unable to read dependency information (file=%q): %v", filename, err)
|
||||||
log.Infof("rust cataloger: unable to read dependency information (file=%q): %v", filename, err)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
@ -18,15 +23,16 @@ import (
|
||||||
"github.com/wagoodman/go-progress/format"
|
"github.com/wagoodman/go-progress/format"
|
||||||
"github.com/wagoodman/jotframe/pkg/frame"
|
"github.com/wagoodman/jotframe/pkg/frame"
|
||||||
|
|
||||||
|
stereoscopeEvent "github.com/anchore/stereoscope/pkg/event"
|
||||||
stereoEventParsers "github.com/anchore/stereoscope/pkg/event/parsers"
|
stereoEventParsers "github.com/anchore/stereoscope/pkg/event/parsers"
|
||||||
"github.com/anchore/stereoscope/pkg/image/docker"
|
"github.com/anchore/stereoscope/pkg/image/docker"
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/internal/ui/components"
|
syftEvent "github.com/anchore/syft/syft/event"
|
||||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxBarWidth = 50
|
const maxBarWidth = 50
|
||||||
const statusSet = components.SpinnerDotSet
|
const statusSet = SpinnerDotSet
|
||||||
const completedStatus = "✔"
|
const completedStatus = "✔"
|
||||||
const failedStatus = "✘"
|
const failedStatus = "✘"
|
||||||
const titleFormat = color.Bold
|
const titleFormat = color.Bold
|
||||||
|
@ -46,16 +52,118 @@ var (
|
||||||
subStatusTitleTemplate = fmt.Sprintf(" └── %%-%ds ", StatusTitleColumn-3)
|
subStatusTitleTemplate = fmt.Sprintf(" └── %%-%ds ", StatusTitleColumn-3)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Handler is an aggregated event handler for the set of supported events (PullDockerImage, ReadImage, FetchImage, PackageCatalogerStarted)
|
||||||
|
// Deprecated: use the bubbletea event handler in cmd/syft/ui/handler.go instead.
|
||||||
|
type Handler struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHandler returns an empty Handler
|
||||||
|
// Deprecated: use the bubbletea event handler in cmd/syft/ui/handler.go instead.
|
||||||
|
func NewHandler() *Handler {
|
||||||
|
return &Handler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RespondsTo indicates if the handler is capable of handling the given event.
|
||||||
|
// Deprecated: use the bubbletea event handler in cmd/syft/ui/handler.go instead.
|
||||||
|
func (r *Handler) RespondsTo(event partybus.Event) bool {
|
||||||
|
switch event.Type {
|
||||||
|
case stereoscopeEvent.PullDockerImage,
|
||||||
|
stereoscopeEvent.ReadImage,
|
||||||
|
stereoscopeEvent.FetchImage,
|
||||||
|
syftEvent.PackageCatalogerStarted,
|
||||||
|
syftEvent.SecretsCatalogerStarted,
|
||||||
|
syftEvent.FileDigestsCatalogerStarted,
|
||||||
|
syftEvent.FileMetadataCatalogerStarted,
|
||||||
|
syftEvent.FileIndexingStarted,
|
||||||
|
syftEvent.AttestationStarted,
|
||||||
|
syftEvent.CatalogerTaskStarted:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle calls the specific event handler for the given event within the context of the screen frame.
|
||||||
|
// Deprecated: use the bubbletea event handler in cmd/syft/ui/handler.go instead.
|
||||||
|
func (r *Handler) Handle(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
|
||||||
|
switch event.Type {
|
||||||
|
case stereoscopeEvent.PullDockerImage:
|
||||||
|
return PullDockerImageHandler(ctx, fr, event, wg)
|
||||||
|
|
||||||
|
case stereoscopeEvent.ReadImage:
|
||||||
|
return ReadImageHandler(ctx, fr, event, wg)
|
||||||
|
|
||||||
|
case stereoscopeEvent.FetchImage:
|
||||||
|
return FetchImageHandler(ctx, fr, event, wg)
|
||||||
|
|
||||||
|
case syftEvent.PackageCatalogerStarted:
|
||||||
|
return PackageCatalogerStartedHandler(ctx, fr, event, wg)
|
||||||
|
|
||||||
|
case syftEvent.SecretsCatalogerStarted:
|
||||||
|
return SecretsCatalogerStartedHandler(ctx, fr, event, wg)
|
||||||
|
|
||||||
|
case syftEvent.FileDigestsCatalogerStarted:
|
||||||
|
return FileDigestsCatalogerStartedHandler(ctx, fr, event, wg)
|
||||||
|
|
||||||
|
case syftEvent.FileMetadataCatalogerStarted:
|
||||||
|
return FileMetadataCatalogerStartedHandler(ctx, fr, event, wg)
|
||||||
|
|
||||||
|
case syftEvent.FileIndexingStarted:
|
||||||
|
return FileIndexingStartedHandler(ctx, fr, event, wg)
|
||||||
|
|
||||||
|
case syftEvent.AttestationStarted:
|
||||||
|
return AttestationStartedHandler(ctx, fr, event, wg)
|
||||||
|
|
||||||
|
case syftEvent.CatalogerTaskStarted:
|
||||||
|
return CatalogerTaskStartedHandler(ctx, fr, event, wg)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
SpinnerDotSet = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
||||||
|
)
|
||||||
|
|
||||||
|
type spinner struct {
|
||||||
|
index int
|
||||||
|
charset []string
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSpinner(charset string) spinner {
|
||||||
|
return spinner{
|
||||||
|
charset: strings.Split(charset, ""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *spinner) Current() string {
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
|
||||||
|
return s.charset[s.index]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *spinner) Next() string {
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
c := s.charset[s.index]
|
||||||
|
s.index++
|
||||||
|
if s.index >= len(s.charset) {
|
||||||
|
s.index = 0
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
// startProcess is a helper function for providing common elements for long-running UI elements (such as a
|
// startProcess is a helper function for providing common elements for long-running UI elements (such as a
|
||||||
// progress bar formatter and status spinner)
|
// progress bar formatter and status spinner)
|
||||||
func startProcess() (format.Simple, *components.Spinner) {
|
func startProcess() (format.Simple, *spinner) {
|
||||||
width, _ := frame.GetTerminalSize()
|
width, _ := frame.GetTerminalSize()
|
||||||
barWidth := int(0.25 * float64(width))
|
barWidth := int(0.25 * float64(width))
|
||||||
if barWidth > maxBarWidth {
|
if barWidth > maxBarWidth {
|
||||||
barWidth = maxBarWidth
|
barWidth = maxBarWidth
|
||||||
}
|
}
|
||||||
formatter := format.NewSimpleWithTheme(barWidth, format.HeavyNoBarTheme, format.ColorCompleted, format.ColorTodo)
|
formatter := format.NewSimpleWithTheme(barWidth, format.HeavyNoBarTheme, format.ColorCompleted, format.ColorTodo)
|
||||||
spinner := components.NewSpinner(statusSet)
|
spinner := newSpinner(statusSet)
|
||||||
|
|
||||||
return formatter, &spinner
|
return formatter, &spinner
|
||||||
}
|
}
|
||||||
|
@ -82,7 +190,7 @@ func formatDockerPullPhase(phase docker.PullPhase, inputStr string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatDockerImagePullStatus writes the docker image pull status summarized into a single line for the given state.
|
// formatDockerImagePullStatus writes the docker image pull status summarized into a single line for the given state.
|
||||||
func formatDockerImagePullStatus(pullStatus *docker.PullStatus, spinner *components.Spinner, line *frame.Line) {
|
func formatDockerImagePullStatus(pullStatus *docker.PullStatus, spinner *spinner, line *frame.Line) {
|
||||||
var size, current uint64
|
var size, current uint64
|
||||||
|
|
||||||
title := titleFormat.Sprint("Pulling image")
|
title := titleFormat.Sprint("Pulling image")
|
||||||
|
@ -491,50 +599,6 @@ func FileDigestsCatalogerStartedHandler(ctx context.Context, fr *frame.Frame, ev
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImportStartedHandler shows the intermittent upload progress to Anchore Enterprise.
|
|
||||||
func ImportStartedHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
|
|
||||||
host, prog, err := syftEventParsers.ParseImportStarted(event)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("bad %s event: %w", event.Type, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
line, err := fr.Append()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
wg.Add(1)
|
|
||||||
|
|
||||||
formatter, spinner := startProcess()
|
|
||||||
stream := progress.Stream(ctx, prog, interval)
|
|
||||||
title := titleFormat.Sprint("Uploading image")
|
|
||||||
|
|
||||||
formatFn := func(p progress.Progress) {
|
|
||||||
progStr, err := formatter.Format(p)
|
|
||||||
spin := color.Magenta.Sprint(spinner.Next())
|
|
||||||
if err != nil {
|
|
||||||
_, _ = io.WriteString(line, fmt.Sprintf("Error: %+v", err))
|
|
||||||
} else {
|
|
||||||
auxInfo := auxInfoFormat.Sprintf("[%s]", prog.Stage())
|
|
||||||
_, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s %s", spin, title, progStr, auxInfo))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
formatFn(progress.Progress{})
|
|
||||||
for p := range stream {
|
|
||||||
formatFn(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
spin := color.Green.Sprint(completedStatus)
|
|
||||||
title = titleFormat.Sprint("Uploaded image")
|
|
||||||
auxInfo := auxInfoFormat.Sprintf("[%s]", host)
|
|
||||||
_, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s", spin, title, auxInfo))
|
|
||||||
}()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// AttestationStartedHandler takes bytes from a event.ShellOutput and publishes them to the frame.
|
// AttestationStartedHandler takes bytes from a event.ShellOutput and publishes them to the frame.
|
||||||
//
|
//
|
||||||
//nolint:funlen,gocognit
|
//nolint:funlen,gocognit
|
|
@ -1,85 +0,0 @@
|
||||||
/*
|
|
||||||
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
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/wagoodman/go-partybus"
|
|
||||||
"github.com/wagoodman/jotframe/pkg/frame"
|
|
||||||
|
|
||||||
stereoscopeEvent "github.com/anchore/stereoscope/pkg/event"
|
|
||||||
syftEvent "github.com/anchore/syft/syft/event"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handler is an aggregated event handler for the set of supported events (PullDockerImage, ReadImage, FetchImage, PackageCatalogerStarted)
|
|
||||||
type Handler struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHandler returns an empty Handler
|
|
||||||
func NewHandler() *Handler {
|
|
||||||
return &Handler{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RespondsTo indicates if the handler is capable of handling the given event.
|
|
||||||
func (r *Handler) RespondsTo(event partybus.Event) bool {
|
|
||||||
switch event.Type {
|
|
||||||
case stereoscopeEvent.PullDockerImage,
|
|
||||||
stereoscopeEvent.ReadImage,
|
|
||||||
stereoscopeEvent.FetchImage,
|
|
||||||
syftEvent.PackageCatalogerStarted,
|
|
||||||
syftEvent.SecretsCatalogerStarted,
|
|
||||||
syftEvent.FileDigestsCatalogerStarted,
|
|
||||||
syftEvent.FileMetadataCatalogerStarted,
|
|
||||||
syftEvent.FileIndexingStarted,
|
|
||||||
syftEvent.ImportStarted,
|
|
||||||
syftEvent.AttestationStarted,
|
|
||||||
syftEvent.CatalogerTaskStarted:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
switch event.Type {
|
|
||||||
case stereoscopeEvent.PullDockerImage:
|
|
||||||
return PullDockerImageHandler(ctx, fr, event, wg)
|
|
||||||
|
|
||||||
case stereoscopeEvent.ReadImage:
|
|
||||||
return ReadImageHandler(ctx, fr, event, wg)
|
|
||||||
|
|
||||||
case stereoscopeEvent.FetchImage:
|
|
||||||
return FetchImageHandler(ctx, fr, event, wg)
|
|
||||||
|
|
||||||
case syftEvent.PackageCatalogerStarted:
|
|
||||||
return PackageCatalogerStartedHandler(ctx, fr, event, wg)
|
|
||||||
|
|
||||||
case syftEvent.SecretsCatalogerStarted:
|
|
||||||
return SecretsCatalogerStartedHandler(ctx, fr, event, wg)
|
|
||||||
|
|
||||||
case syftEvent.FileDigestsCatalogerStarted:
|
|
||||||
return FileDigestsCatalogerStartedHandler(ctx, fr, event, wg)
|
|
||||||
|
|
||||||
case syftEvent.FileMetadataCatalogerStarted:
|
|
||||||
return FileMetadataCatalogerStartedHandler(ctx, fr, event, wg)
|
|
||||||
|
|
||||||
case syftEvent.FileIndexingStarted:
|
|
||||||
return FileIndexingStartedHandler(ctx, fr, event, wg)
|
|
||||||
|
|
||||||
case syftEvent.ImportStarted:
|
|
||||||
return ImportStartedHandler(ctx, fr, event, wg)
|
|
||||||
|
|
||||||
case syftEvent.AttestationStarted:
|
|
||||||
return AttestationStartedHandler(ctx, fr, event, wg)
|
|
||||||
|
|
||||||
case syftEvent.CatalogerTaskStarted:
|
|
||||||
return CatalogerTaskStartedHandler(ctx, fr, event, wg)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
Loading…
Reference in a new issue