mirror of
https://github.com/anchore/syft.git
synced 2026-10-31 22:46:36 +01: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:
@@ -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]
|
||||
}
|
||||
Reference in New Issue
Block a user