2020-07-30 19:16:58 +00:00
|
|
|
package ui
|
2020-06-25 14:39:11 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
2020-08-03 20:03:47 +00:00
|
|
|
"strings"
|
2020-06-25 14:39:11 +00:00
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
2020-08-03 20:03:47 +00:00
|
|
|
"github.com/anchore/stereoscope/pkg/image/docker"
|
|
|
|
"github.com/dustin/go-humanize"
|
|
|
|
|
2020-06-25 14:39:11 +00:00
|
|
|
stereoEventParsers "github.com/anchore/stereoscope/pkg/event/parsers"
|
2020-07-30 19:16:58 +00:00
|
|
|
"github.com/anchore/syft/internal/ui/common"
|
2020-07-24 00:54:04 +00:00
|
|
|
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
2020-06-25 14:39:11 +00:00
|
|
|
"github.com/gookit/color"
|
|
|
|
"github.com/wagoodman/go-partybus"
|
|
|
|
"github.com/wagoodman/go-progress"
|
|
|
|
"github.com/wagoodman/go-progress/format"
|
|
|
|
"github.com/wagoodman/jotframe/pkg/frame"
|
|
|
|
)
|
|
|
|
|
|
|
|
const maxBarWidth = 50
|
2020-07-30 19:16:58 +00:00
|
|
|
const statusSet = common.SpinnerDotSet // SpinnerCircleOutlineSet
|
|
|
|
const completedStatus = "✔" // "●"
|
2020-06-25 14:39:11 +00:00
|
|
|
const tileFormat = color.Bold
|
|
|
|
const statusTitleTemplate = " %s %-28s "
|
2020-08-03 20:03:47 +00:00
|
|
|
const interval = 150 * time.Millisecond
|
2020-06-25 14:39:11 +00:00
|
|
|
|
2020-08-03 20:03:47 +00:00
|
|
|
var (
|
|
|
|
auxInfoFormat = color.HEX("#777777")
|
|
|
|
dockerPullCompletedColor = color.HEX("#fcba03")
|
|
|
|
dockerPullDownloadColor = color.HEX("#777777")
|
|
|
|
dockerPullExtractColor = color.White
|
|
|
|
dockerPullStageChars = strings.Split("▁▃▄▅▆▇█", "")
|
|
|
|
)
|
2020-06-25 14:39:11 +00:00
|
|
|
|
2020-08-12 15:04:39 +00:00
|
|
|
// startProcess is a helper function for providing common elements for long-running UI elements (such as a
|
|
|
|
// progress bar formatter and status spinner)
|
2020-07-30 19:16:58 +00:00
|
|
|
func startProcess() (format.Simple, *common.Spinner) {
|
2020-06-25 14:39:11 +00:00
|
|
|
width, _ := frame.GetTerminalSize()
|
|
|
|
barWidth := int(0.25 * float64(width))
|
|
|
|
if barWidth > maxBarWidth {
|
|
|
|
barWidth = maxBarWidth
|
|
|
|
}
|
|
|
|
formatter := format.NewSimpleWithTheme(barWidth, format.HeavyNoBarTheme, format.ColorCompleted, format.ColorTodo)
|
2020-07-30 19:16:58 +00:00
|
|
|
spinner := common.NewSpinner(statusSet)
|
2020-06-25 14:39:11 +00:00
|
|
|
|
|
|
|
return formatter, &spinner
|
|
|
|
}
|
|
|
|
|
2020-08-12 15:04:39 +00:00
|
|
|
// formatDockerPullPhase returns a single character that represents the status of a layer pull.
|
2020-08-03 20:03:47 +00:00
|
|
|
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
|
2020-08-12 15:04:39 +00:00
|
|
|
// formatDockerImagePullStatus writes the docker image pull status summarized into a single line for the given state.
|
2020-08-03 20:03:47 +00:00
|
|
|
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))
|
|
|
|
}
|
|
|
|
|
2020-08-12 15:04:39 +00:00
|
|
|
// PullDockerImageHandler periodically writes a formatted line widget representing a docker image pull event.
|
2020-08-03 20:03:47 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-08-12 15:04:39 +00:00
|
|
|
// FetchImageHandler periodically writes a the image save and write-to-disk process in the form of a progress bar.
|
2020-12-10 03:20:53 +00:00
|
|
|
// nolint:dupl
|
2020-07-30 19:16:58 +00:00
|
|
|
func FetchImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
|
2020-06-25 14:39:11 +00:00
|
|
|
_, prog, err := stereoEventParsers.ParseFetchImage(event)
|
|
|
|
if err != nil {
|
2020-08-03 20:03:47 +00:00
|
|
|
return fmt.Errorf("bad %s event: %w", event.Type, err)
|
2020-06-25 14:39:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
line, err := fr.Append()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
wg.Add(1)
|
|
|
|
|
2020-07-31 21:53:22 +00:00
|
|
|
formatter, spinner := startProcess()
|
2020-08-03 20:03:47 +00:00
|
|
|
stream := progress.Stream(ctx, prog, interval)
|
2020-08-06 12:19:03 +00:00
|
|
|
title := tileFormat.Sprint("Loading image")
|
2020-07-31 21:53:22 +00:00
|
|
|
|
|
|
|
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))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-25 14:39:11 +00:00
|
|
|
go func() {
|
|
|
|
defer wg.Done()
|
|
|
|
|
2020-07-31 21:53:22 +00:00
|
|
|
formatFn(progress.Progress{})
|
2020-06-25 14:39:11 +00:00
|
|
|
for p := range stream {
|
2020-07-31 21:53:22 +00:00
|
|
|
formatFn(p)
|
2020-06-25 14:39:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
spin := color.Green.Sprint(completedStatus)
|
2020-08-06 12:19:03 +00:00
|
|
|
title = tileFormat.Sprint("Loaded image")
|
2020-06-25 14:39:11 +00:00
|
|
|
_, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate, spin, title))
|
|
|
|
}()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-08-12 15:04:39 +00:00
|
|
|
// ReadImageHandler periodically writes a the image read/parse/build-tree status in the form of a progress bar.
|
2020-07-30 19:16:58 +00:00
|
|
|
func ReadImageHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
|
2020-06-25 14:39:11 +00:00
|
|
|
_, prog, err := stereoEventParsers.ParseReadImage(event)
|
|
|
|
if err != nil {
|
2020-08-03 20:03:47 +00:00
|
|
|
return fmt.Errorf("bad %s event: %w", event.Type, err)
|
2020-06-25 14:39:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
line, err := fr.Append()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
2020-07-31 21:53:22 +00:00
|
|
|
formatter, spinner := startProcess()
|
2020-08-03 20:03:47 +00:00
|
|
|
stream := progress.Stream(ctx, prog, interval)
|
2020-08-06 12:19:03 +00:00
|
|
|
title := tileFormat.Sprint("Parsing image")
|
2020-07-31 21:53:22 +00:00
|
|
|
|
|
|
|
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 {
|
|
|
|
_, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s", spin, title, progStr))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-25 14:39:11 +00:00
|
|
|
go func() {
|
|
|
|
defer wg.Done()
|
|
|
|
|
2020-07-31 21:53:22 +00:00
|
|
|
formatFn(progress.Progress{})
|
2020-06-25 14:39:11 +00:00
|
|
|
for p := range stream {
|
2020-07-31 21:53:22 +00:00
|
|
|
formatFn(p)
|
2020-06-25 14:39:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
spin := color.Green.Sprint(completedStatus)
|
2020-08-06 12:19:03 +00:00
|
|
|
title = tileFormat.Sprint("Parsed image")
|
2020-06-25 14:39:11 +00:00
|
|
|
_, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate, spin, title))
|
|
|
|
}()
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-08-12 15:04:39 +00:00
|
|
|
// CatalogerStartedHandler periodically writes catalog statistics to a single line.
|
2020-07-30 19:16:58 +00:00
|
|
|
func CatalogerStartedHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
|
2020-07-24 00:54:04 +00:00
|
|
|
monitor, err := syftEventParsers.ParseCatalogerStarted(event)
|
2020-06-25 14:39:11 +00:00
|
|
|
if err != nil {
|
2020-08-03 20:03:47 +00:00
|
|
|
return fmt.Errorf("bad %s event: %w", event.Type, err)
|
2020-06-25 14:39:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
line, err := fr.Append()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
2020-07-31 21:53:22 +00:00
|
|
|
_, spinner := startProcess()
|
2020-08-03 20:03:47 +00:00
|
|
|
stream := progress.StreamMonitors(ctx, []progress.Monitorable{monitor.FilesProcessed, monitor.PackagesDiscovered}, interval)
|
|
|
|
title := tileFormat.Sprint("Cataloging image")
|
2020-07-31 21:53:22 +00:00
|
|
|
|
|
|
|
formatFn := func(p int64) {
|
|
|
|
spin := color.Magenta.Sprint(spinner.Next())
|
|
|
|
auxInfo := auxInfoFormat.Sprintf("[packages %d]", p)
|
|
|
|
_, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s", spin, title, auxInfo))
|
|
|
|
}
|
|
|
|
|
2020-06-25 14:39:11 +00:00
|
|
|
go func() {
|
|
|
|
defer wg.Done()
|
|
|
|
|
2020-07-31 21:53:22 +00:00
|
|
|
formatFn(0)
|
2020-06-25 14:39:11 +00:00
|
|
|
for p := range stream {
|
2020-07-31 21:53:22 +00:00
|
|
|
formatFn(p[1])
|
2020-06-25 14:39:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
spin := color.Green.Sprint(completedStatus)
|
|
|
|
title = tileFormat.Sprint("Cataloged image")
|
|
|
|
auxInfo := auxInfoFormat.Sprintf("[%d packages]", monitor.PackagesDiscovered.Current())
|
|
|
|
_, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s", spin, title, auxInfo))
|
|
|
|
}()
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2020-12-10 03:20:53 +00:00
|
|
|
|
|
|
|
// ImportStartedHandler shows the intermittent upload progress to Anchore Enterprise.
|
|
|
|
// nolint:dupl
|
|
|
|
func ImportStartedHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error {
|
2020-12-18 21:59:30 +00:00
|
|
|
host, prog, err := syftEventParsers.ParseImportStarted(event)
|
2020-12-10 03:20:53 +00:00
|
|
|
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 := tileFormat.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 = tileFormat.Sprint("Uploaded image")
|
2020-12-18 21:59:30 +00:00
|
|
|
auxInfo := auxInfoFormat.Sprintf("[%s]", host)
|
|
|
|
_, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s", spin, title, auxInfo))
|
2020-12-10 03:20:53 +00:00
|
|
|
}()
|
|
|
|
return err
|
|
|
|
}
|