mirror of
https://github.com/anchore/syft.git
synced 2026-11-01 06:56:40 +01:00
bump stereoscope for docker pull + add UI elements for pull status (#117)
This commit is contained in:
+148
-10
@@ -4,9 +4,13 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/image/docker"
|
||||
"github.com/dustin/go-humanize"
|
||||
|
||||
stereoEventParsers "github.com/anchore/stereoscope/pkg/event/parsers"
|
||||
"github.com/anchore/syft/internal/ui/common"
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
@@ -22,8 +26,15 @@ const statusSet = common.SpinnerDotSet // SpinnerCircleOutlineSet
|
||||
const completedStatus = "✔" // "●"
|
||||
const tileFormat = color.Bold
|
||||
const statusTitleTemplate = " %s %-28s "
|
||||
const interval = 150 * time.Millisecond
|
||||
|
||||
var auxInfoFormat = color.HEX("#777777")
|
||||
var (
|
||||
auxInfoFormat = color.HEX("#777777")
|
||||
dockerPullCompletedColor = color.HEX("#fcba03")
|
||||
dockerPullDownloadColor = color.HEX("#777777")
|
||||
dockerPullExtractColor = color.White
|
||||
dockerPullStageChars = strings.Split("▁▃▄▅▆▇█", "")
|
||||
)
|
||||
|
||||
func startProcess() (format.Simple, *common.Spinner) {
|
||||
width, _ := frame.GetTerminalSize()
|
||||
@@ -37,10 +48,137 @@ func startProcess() (format.Simple, *common.Spinner) {
|
||||
return formatter, &spinner
|
||||
}
|
||||
|
||||
func 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 dockerPullDownloadColor.Sprint(inputStr)
|
||||
case docker.DownloadCompletePhase:
|
||||
return dockerPullDownloadColor.Sprint(dockerPullStageChars[len(dockerPullStageChars)-1])
|
||||
case docker.ExtractingPhase:
|
||||
return dockerPullExtractColor.Sprint(inputStr)
|
||||
case docker.VerifyingChecksumPhase, docker.PullCompletePhase:
|
||||
return dockerPullCompletedColor.Sprint(inputStr)
|
||||
case docker.AlreadyExistsPhase:
|
||||
return dockerPullCompletedColor.Sprint(dockerPullStageChars[len(dockerPullStageChars)-1])
|
||||
default:
|
||||
return inputStr
|
||||
}
|
||||
}
|
||||
|
||||
// nolint:funlen
|
||||
func formatDockerImagePullStatus(pullStatus *docker.PullStatus, spinner *common.Spinner, line *frame.Line) {
|
||||
var size, current uint64
|
||||
|
||||
title := tileFormat.Sprint("Pulling image")
|
||||
|
||||
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
|
||||
current := prog.Current()
|
||||
size := prog.Size()
|
||||
|
||||
if progress.IsCompleted(prog) {
|
||||
input := dockerPullStageChars[len(dockerPullStageChars)-1]
|
||||
completed[idx] = formatDockerPullPhase(status[layer].Phase, input)
|
||||
} else if current != 0 {
|
||||
var ratio float64
|
||||
switch {
|
||||
case current == 0 || size < 0:
|
||||
ratio = 0
|
||||
case current >= size:
|
||||
ratio = 1
|
||||
default:
|
||||
ratio = float64(current) / float64(size)
|
||||
}
|
||||
|
||||
i := int(ratio * float64(len(dockerPullStageChars)-1))
|
||||
input := dockerPullStageChars[i]
|
||||
completed[idx] = 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 := dockerPullCompletedColor.Sprintf("%d Layers", len(layers))
|
||||
auxInfo = auxInfoFormat.Sprintf("[%s / %s]", humanize.Bytes(current), humanize.Bytes(size))
|
||||
if len(layers) == numCompleted {
|
||||
auxInfo = auxInfoFormat.Sprintf("[%s] Extracting...", humanize.Bytes(size))
|
||||
}
|
||||
|
||||
progStr = fmt.Sprintf("%s▕%s▏", prefix, render)
|
||||
}
|
||||
|
||||
spin := color.Magenta.Sprint(spinner.Next())
|
||||
_, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s%s", spin, title, progStr, auxInfo))
|
||||
}
|
||||
|
||||
func PullDockerImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
|
||||
_, pullStatus, err := stereoEventParsers.ParsePullDockerImage(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)
|
||||
|
||||
_, spinner := startProcess()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break loop
|
||||
case <-time.After(interval):
|
||||
formatDockerImagePullStatus(pullStatus, spinner, line)
|
||||
if pullStatus.Complete() {
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pullStatus.Complete() {
|
||||
spin := color.Green.Sprint(completedStatus)
|
||||
title := tileFormat.Sprint("Pulled image")
|
||||
_, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate, spin, title))
|
||||
}
|
||||
}()
|
||||
return err
|
||||
}
|
||||
|
||||
func FetchImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
|
||||
_, prog, err := stereoEventParsers.ParseFetchImage(event)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad FetchImage event: %w", err)
|
||||
return fmt.Errorf("bad %s event: %w", event.Type, err)
|
||||
}
|
||||
|
||||
line, err := fr.Append()
|
||||
@@ -50,8 +188,8 @@ func FetchImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Even
|
||||
wg.Add(1)
|
||||
|
||||
formatter, spinner := startProcess()
|
||||
stream := progress.Stream(ctx, prog, 150*time.Millisecond)
|
||||
title := tileFormat.Sprint("Fetching image...")
|
||||
stream := progress.Stream(ctx, prog, interval)
|
||||
title := tileFormat.Sprint("Fetching image")
|
||||
|
||||
formatFn := func(p progress.Progress) {
|
||||
progStr, err := formatter.Format(p)
|
||||
@@ -82,7 +220,7 @@ func FetchImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Even
|
||||
func ReadImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
|
||||
_, prog, err := stereoEventParsers.ParseReadImage(event)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad ReadImage event: %w", err)
|
||||
return fmt.Errorf("bad %s event: %w", event.Type, err)
|
||||
}
|
||||
|
||||
line, err := fr.Append()
|
||||
@@ -93,8 +231,8 @@ func ReadImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event
|
||||
wg.Add(1)
|
||||
|
||||
formatter, spinner := startProcess()
|
||||
stream := progress.Stream(ctx, prog, 150*time.Millisecond)
|
||||
title := tileFormat.Sprint("Reading image...")
|
||||
stream := progress.Stream(ctx, prog, interval)
|
||||
title := tileFormat.Sprint("Reading image")
|
||||
|
||||
formatFn := func(p progress.Progress) {
|
||||
progStr, err := formatter.Format(p)
|
||||
@@ -125,7 +263,7 @@ func ReadImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event
|
||||
func CatalogerStartedHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
|
||||
monitor, err := syftEventParsers.ParseCatalogerStarted(event)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad CatalogerStarted event: %w", err)
|
||||
return fmt.Errorf("bad %s event: %w", event.Type, err)
|
||||
}
|
||||
|
||||
line, err := fr.Append()
|
||||
@@ -136,8 +274,8 @@ func CatalogerStartedHandler(ctx context.Context, fr *frame.Frame, event partybu
|
||||
wg.Add(1)
|
||||
|
||||
_, spinner := startProcess()
|
||||
stream := progress.StreamMonitors(ctx, []progress.Monitorable{monitor.FilesProcessed, monitor.PackagesDiscovered}, 50*time.Millisecond)
|
||||
title := tileFormat.Sprint("Cataloging image...")
|
||||
stream := progress.StreamMonitors(ctx, []progress.Monitorable{monitor.FilesProcessed, monitor.PackagesDiscovered}, interval)
|
||||
title := tileFormat.Sprint("Cataloging image")
|
||||
|
||||
formatFn := func(p int64) {
|
||||
spin := color.Magenta.Sprint(spinner.Next())
|
||||
|
||||
+4
-1
@@ -19,7 +19,7 @@ func NewHandler() *Handler {
|
||||
|
||||
func (r *Handler) RespondsTo(event partybus.Event) bool {
|
||||
switch event.Type {
|
||||
case stereoscopeEvent.ReadImage, stereoscopeEvent.FetchImage, syftEvent.CatalogerStarted:
|
||||
case stereoscopeEvent.PullDockerImage, stereoscopeEvent.ReadImage, stereoscopeEvent.FetchImage, syftEvent.CatalogerStarted:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -28,6 +28,9 @@ func (r *Handler) RespondsTo(event partybus.Event) bool {
|
||||
|
||||
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user