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/containerd" "github.com/anchore/syft/internal/log" ) var _ interface { progress.Stager progress.Progressable } = (*containerdPullProgressAdapter)(nil) type containerdPullStatus interface { Complete() bool Layers() []containerd.LayerID Current(containerd.LayerID) progress.Progressable } type containerdPullProgressAdapter struct { status containerdPullStatus formatter containerdPullStatusFormatter } type containerdPullStatusFormatter struct { auxInfoStyle lipgloss.Style pullCompletedStyle lipgloss.Style pullDownloadStyle lipgloss.Style pullStageChars []string layerCaps []string } func (m *Handler) handlePullContainerdImage(e partybus.Event) []tea.Model { _, pullStatus, err := stereoscopeParsers.ParsePullContainerdImage(e) if err != nil { log.WithFields("error", err).Warn("unable to parse event") return nil } if pullStatus == nil { return nil } tsk := m.newTaskProgress( taskprogress.Title{ Default: "Pull image", Running: "Pulling image", Success: "Pulled image", }, taskprogress.WithStagedProgressable( newContainerdPullProgressAdapter(pullStatus), ), ) tsk.HintStyle = lipgloss.NewStyle() tsk.HintEndCaps = nil return []tea.Model{tsk} } func newContainerdPullProgressAdapter(status *containerd.PullStatus) *containerdPullProgressAdapter { return &containerdPullProgressAdapter{ status: status, formatter: newContainerdPullStatusFormatter(), } } func newContainerdPullStatusFormatter() containerdPullStatusFormatter { return containerdPullStatusFormatter{ auxInfoStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")), pullCompletedStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#fcba03")), pullDownloadStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")), pullStageChars: strings.Split("▁▃▄▅▆▇█", ""), layerCaps: strings.Split("▕▏", ""), } } func (d containerdPullProgressAdapter) Size() int64 { return -1 } func (d containerdPullProgressAdapter) Current() int64 { return 1 } func (d containerdPullProgressAdapter) Error() error { if d.status.Complete() { return progress.ErrCompleted } // TODO: return intermediate error indications return nil } func (d containerdPullProgressAdapter) Stage() string { return d.formatter.Render(d.status) } // Render crafts the given docker image pull status summarized into a single line. func (f containerdPullStatusFormatter) Render(pullStatus containerdPullStatus) string { var size, current uint64 layers := pullStatus.Layers() status := make(map[containerd.LayerID]progress.Progressable) 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] curN := prog.Current() curSize := prog.Size() if progress.IsCompleted(prog) { input := f.pullStageChars[len(f.pullStageChars)-1] completed[idx] = f.formatPullPhase(prog.Error() != nil, 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.pullStageChars)-1)) input := f.pullStageChars[i] completed[idx] = f.formatPullPhase(status[layer].Error() != nil, input) } if progress.IsErrCompleted(status[layer].Error()) { numCompleted++ } } for _, layer := range layers { prog := status[layer] size += uint64(prog.Size()) current += uint64(prog.Current()) } var progStr, auxInfo string if len(layers) > 0 { render := strings.Join(completed, "") prefix := f.pullCompletedStyle.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 } // formatPullPhase returns a single character that represents the status of a layer pull. func (f containerdPullStatusFormatter) formatPullPhase(completed bool, inputStr string) string { if completed { return f.pullCompletedStyle.Render(f.pullStageChars[len(f.pullStageChars)-1]) } return f.pullDownloadStyle.Render(inputStr) } func (f containerdPullStatusFormatter) layerCap(end bool) string { l := len(f.layerCaps) if l == 0 { return "" } if end { return f.layerCaps[l-1] } return f.layerCaps[0] }