github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/cmd/syft/cli/ui/handle_attestation.go (about)

     1  package ui
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	tea "github.com/charmbracelet/bubbletea"
    12  	"github.com/charmbracelet/lipgloss"
    13  	"github.com/google/uuid"
    14  	"github.com/wagoodman/go-partybus"
    15  	"github.com/wagoodman/go-progress"
    16  	"github.com/zyedidia/generic/queue"
    17  
    18  	"github.com/anchore/bubbly/bubbles/taskprogress"
    19  	"github.com/anchore/syft/internal/log"
    20  	syftEventParsers "github.com/anchore/syft/syft/event/parsers"
    21  )
    22  
    23  var (
    24  	_ tea.Model = (*attestLogFrame)(nil)
    25  )
    26  
    27  type attestLogFrame struct {
    28  	reader     *backgroundLineReader
    29  	prog       progress.Progressable
    30  	lines      []string
    31  	completed  bool
    32  	failed     bool
    33  	windowSize tea.WindowSizeMsg
    34  
    35  	id       uint32
    36  	sequence int
    37  
    38  	updateDuration time.Duration
    39  	borderStype    lipgloss.Style
    40  }
    41  
    42  // attestLogFrameTickMsg indicates that the timer has ticked and we should render a frame.
    43  type attestLogFrameTickMsg struct {
    44  	Time     time.Time
    45  	Sequence int
    46  	ID       uint32
    47  }
    48  
    49  type backgroundLineReader struct {
    50  	limit int
    51  	lines *queue.Queue[string]
    52  	lock  *sync.RWMutex
    53  
    54  	// This is added specifically for tests to assert when the background reader is done.
    55  	// The main UI uses the global ui wait group from the handler to otherwise block
    56  	// Shared concerns among multiple model made it difficult to test using the global wait group
    57  	// so this is added to allow tests to assert when the background reader is done.
    58  	running *sync.WaitGroup
    59  }
    60  
    61  func (m *Handler) handleAttestationStarted(e partybus.Event) []tea.Model {
    62  	reader, prog, taskInfo, err := syftEventParsers.ParseAttestationStartedEvent(e)
    63  	if err != nil {
    64  		log.WithFields("error", err).Warn("unable to parse event")
    65  		return nil
    66  	}
    67  
    68  	stage := progress.Stage{}
    69  
    70  	tsk := m.newTaskProgress(
    71  		taskprogress.Title{
    72  			Default: taskInfo.Title.Default,
    73  			Running: taskInfo.Title.WhileRunning,
    74  			Success: taskInfo.Title.OnSuccess,
    75  		},
    76  		taskprogress.WithStagedProgressable(
    77  			struct {
    78  				progress.Progressable
    79  				progress.Stager
    80  			}{
    81  				Progressable: prog,
    82  				Stager:       &stage,
    83  			},
    84  		),
    85  	)
    86  
    87  	tsk.HideStageOnSuccess = false
    88  
    89  	if taskInfo.Context != "" {
    90  		tsk.Context = []string{taskInfo.Context}
    91  	}
    92  
    93  	borderStyle := tsk.HintStyle
    94  
    95  	return []tea.Model{
    96  		tsk,
    97  		newLogFrame(newBackgroundLineReader(m.Running, reader, &stage), prog, borderStyle),
    98  	}
    99  }
   100  
   101  func newLogFrame(reader *backgroundLineReader, prog progress.Progressable, borderStyle lipgloss.Style) attestLogFrame {
   102  	return attestLogFrame{
   103  		reader:         reader,
   104  		prog:           prog,
   105  		id:             uuid.Must(uuid.NewUUID()).ID(),
   106  		updateDuration: 250 * time.Millisecond,
   107  		borderStype:    borderStyle,
   108  	}
   109  }
   110  
   111  func newBackgroundLineReader(wg *sync.WaitGroup, reader io.Reader, stage *progress.Stage) *backgroundLineReader {
   112  	r := &backgroundLineReader{
   113  		limit:   7,
   114  		lock:    &sync.RWMutex{},
   115  		lines:   queue.New[string](),
   116  		running: &sync.WaitGroup{},
   117  	}
   118  
   119  	// tracks the background reader for the global handler wait group
   120  	wg.Add(1)
   121  
   122  	// tracks the background reader for the local wait group (used in tests to decouple from the global handler wait group)
   123  	r.running.Add(1)
   124  
   125  	go func() {
   126  		r.read(reader, stage)
   127  		wg.Done()
   128  		r.running.Done()
   129  	}()
   130  
   131  	return r
   132  }
   133  
   134  func (l *backgroundLineReader) read(reader io.Reader, stage *progress.Stage) {
   135  	s := bufio.NewScanner(reader)
   136  
   137  	for s.Scan() {
   138  		l.lock.Lock()
   139  
   140  		text := s.Text()
   141  		l.lines.Enqueue(text)
   142  
   143  		if strings.Contains(text, "tlog entry created with index") {
   144  			fields := strings.SplitN(text, ":", 2)
   145  			present := text
   146  			if len(fields) == 2 {
   147  				present = fmt.Sprintf("transparency log index: %s", fields[1])
   148  			}
   149  			stage.Current = present
   150  		} else if strings.Contains(text, "WARNING: skipping transparency log upload") {
   151  			stage.Current = "transparency log upload skipped"
   152  		}
   153  
   154  		// only show the last X lines of the shell output
   155  		for l.lines.Len() > l.limit {
   156  			l.lines.Dequeue()
   157  		}
   158  
   159  		l.lock.Unlock()
   160  	}
   161  }
   162  
   163  func (l backgroundLineReader) Lines() []string {
   164  	l.lock.RLock()
   165  	defer l.lock.RUnlock()
   166  
   167  	var lines []string
   168  
   169  	l.lines.Each(func(line string) {
   170  		lines = append(lines, line)
   171  	})
   172  
   173  	return lines
   174  }
   175  
   176  func (l attestLogFrame) Init() tea.Cmd {
   177  	// this is the periodic update of state information
   178  	return func() tea.Msg {
   179  		return attestLogFrameTickMsg{
   180  			// The time at which the tick occurred.
   181  			Time: time.Now(),
   182  
   183  			// The ID of the log frame that this message belongs to. This can be
   184  			// helpful when routing messages, however bear in mind that log frames
   185  			// will ignore messages that don't contain ID by default.
   186  			ID: l.id,
   187  
   188  			Sequence: l.sequence,
   189  		}
   190  	}
   191  }
   192  
   193  func (l attestLogFrame) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
   194  	switch msg := msg.(type) {
   195  	case tea.WindowSizeMsg:
   196  		l.windowSize = msg
   197  		return l, nil
   198  
   199  	case attestLogFrameTickMsg:
   200  		l.lines = l.reader.Lines()
   201  
   202  		l.completed = progress.IsCompleted(l.prog)
   203  		err := l.prog.Error()
   204  		l.failed = err != nil && !progress.IsErrCompleted(err)
   205  
   206  		tickCmd := l.handleTick(msg)
   207  
   208  		return l, tickCmd
   209  	}
   210  
   211  	return l, nil
   212  }
   213  
   214  func (l attestLogFrame) View() string {
   215  	if l.completed && !l.failed {
   216  		return ""
   217  	}
   218  
   219  	sb := strings.Builder{}
   220  
   221  	for _, line := range l.lines {
   222  		sb.WriteString(fmt.Sprintf("     %s %s\n", l.borderStype.Render("░░"), line))
   223  	}
   224  
   225  	return sb.String()
   226  }
   227  
   228  func (l attestLogFrame) queueNextTick() tea.Cmd {
   229  	return tea.Tick(l.updateDuration, func(t time.Time) tea.Msg {
   230  		return attestLogFrameTickMsg{
   231  			Time:     t,
   232  			ID:       l.id,
   233  			Sequence: l.sequence,
   234  		}
   235  	})
   236  }
   237  
   238  func (l *attestLogFrame) handleTick(msg attestLogFrameTickMsg) tea.Cmd {
   239  	// If an ID is set, and the ID doesn't belong to this log frame, reject the message.
   240  	if msg.ID > 0 && msg.ID != l.id {
   241  		return nil
   242  	}
   243  
   244  	// If a sequence is set, and it's not the one we expect, reject the message.
   245  	// This prevents the log frame from receiving too many messages and
   246  	// thus updating too frequently.
   247  	if msg.Sequence > 0 && msg.Sequence != l.sequence {
   248  		return nil
   249  	}
   250  
   251  	l.sequence++
   252  
   253  	// note: even if the log is completed we should still respond to stage changes and window size events
   254  	return l.queueNextTick()
   255  }