mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 08:23:15 +01:00
* 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>
248 lines
5.4 KiB
Go
248 lines
5.4 KiB
Go
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()
|
|
}
|