github.com/lineaje-labs/syft@v0.98.1-0.20231227153149-9e393f60ff1b/cmd/syft/cli/ui/handle_pull_containerd_image.go (about)

     1  package ui
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  
     7  	tea "github.com/charmbracelet/bubbletea"
     8  	"github.com/charmbracelet/lipgloss"
     9  	"github.com/dustin/go-humanize"
    10  	"github.com/wagoodman/go-partybus"
    11  	"github.com/wagoodman/go-progress"
    12  
    13  	"github.com/anchore/bubbly/bubbles/taskprogress"
    14  	stereoscopeParsers "github.com/anchore/stereoscope/pkg/event/parsers"
    15  	"github.com/anchore/stereoscope/pkg/image/containerd"
    16  	"github.com/lineaje-labs/syft/internal/log"
    17  )
    18  
    19  var _ interface {
    20  	progress.Stager
    21  	progress.Progressable
    22  } = (*containerdPullProgressAdapter)(nil)
    23  
    24  type containerdPullStatus interface {
    25  	Complete() bool
    26  	Layers() []containerd.LayerID
    27  	Current(containerd.LayerID) progress.Progressable
    28  }
    29  
    30  type containerdPullProgressAdapter struct {
    31  	status    containerdPullStatus
    32  	formatter containerdPullStatusFormatter
    33  }
    34  
    35  type containerdPullStatusFormatter struct {
    36  	auxInfoStyle       lipgloss.Style
    37  	pullCompletedStyle lipgloss.Style
    38  	pullDownloadStyle  lipgloss.Style
    39  	pullStageChars     []string
    40  	layerCaps          []string
    41  }
    42  
    43  func (m *Handler) handlePullContainerdImage(e partybus.Event) []tea.Model {
    44  	_, pullStatus, err := stereoscopeParsers.ParsePullContainerdImage(e)
    45  	if err != nil {
    46  		log.WithFields("error", err).Warn("unable to parse event")
    47  		return nil
    48  	}
    49  
    50  	if pullStatus == nil {
    51  		return nil
    52  	}
    53  
    54  	tsk := m.newTaskProgress(
    55  		taskprogress.Title{
    56  			Default: "Pull image",
    57  			Running: "Pulling image",
    58  			Success: "Pulled image",
    59  		},
    60  		taskprogress.WithStagedProgressable(
    61  			newContainerdPullProgressAdapter(pullStatus),
    62  		),
    63  	)
    64  
    65  	tsk.HintStyle = lipgloss.NewStyle()
    66  	tsk.HintEndCaps = nil
    67  
    68  	return []tea.Model{tsk}
    69  }
    70  
    71  func newContainerdPullProgressAdapter(status *containerd.PullStatus) *containerdPullProgressAdapter {
    72  	return &containerdPullProgressAdapter{
    73  		status:    status,
    74  		formatter: newContainerdPullStatusFormatter(),
    75  	}
    76  }
    77  
    78  func newContainerdPullStatusFormatter() containerdPullStatusFormatter {
    79  	return containerdPullStatusFormatter{
    80  		auxInfoStyle:       lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")),
    81  		pullCompletedStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#fcba03")),
    82  		pullDownloadStyle:  lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")),
    83  		pullStageChars:     strings.Split("▁▃▄▅▆▇█", ""),
    84  		layerCaps:          strings.Split("▕▏", ""),
    85  	}
    86  }
    87  
    88  func (d containerdPullProgressAdapter) Size() int64 {
    89  	return -1
    90  }
    91  
    92  func (d containerdPullProgressAdapter) Current() int64 {
    93  	return 1
    94  }
    95  
    96  func (d containerdPullProgressAdapter) Error() error {
    97  	if d.status.Complete() {
    98  		return progress.ErrCompleted
    99  	}
   100  	// TODO: return intermediate error indications
   101  	return nil
   102  }
   103  
   104  func (d containerdPullProgressAdapter) Stage() string {
   105  	return d.formatter.Render(d.status)
   106  }
   107  
   108  // Render crafts the given docker image pull status summarized into a single line.
   109  func (f containerdPullStatusFormatter) Render(pullStatus containerdPullStatus) string {
   110  	var size, current uint64
   111  
   112  	layers := pullStatus.Layers()
   113  	status := make(map[containerd.LayerID]progress.Progressable)
   114  	completed := make([]string, len(layers))
   115  
   116  	// fetch the current state
   117  	for idx, layer := range layers {
   118  		completed[idx] = " "
   119  		status[layer] = pullStatus.Current(layer)
   120  	}
   121  
   122  	numCompleted := 0
   123  	for idx, layer := range layers {
   124  		prog := status[layer]
   125  		curN := prog.Current()
   126  		curSize := prog.Size()
   127  
   128  		if progress.IsCompleted(prog) {
   129  			input := f.pullStageChars[len(f.pullStageChars)-1]
   130  			completed[idx] = f.formatPullPhase(prog.Error() != nil, input)
   131  		} else if curN != 0 {
   132  			var ratio float64
   133  			switch {
   134  			case curN == 0 || curSize < 0:
   135  				ratio = 0
   136  			case curN >= curSize:
   137  				ratio = 1
   138  			default:
   139  				ratio = float64(curN) / float64(curSize)
   140  			}
   141  
   142  			i := int(ratio * float64(len(f.pullStageChars)-1))
   143  			input := f.pullStageChars[i]
   144  			completed[idx] = f.formatPullPhase(status[layer].Error() != nil, input)
   145  		}
   146  
   147  		if progress.IsErrCompleted(status[layer].Error()) {
   148  			numCompleted++
   149  		}
   150  	}
   151  
   152  	for _, layer := range layers {
   153  		prog := status[layer]
   154  		size += uint64(prog.Size())
   155  		current += uint64(prog.Current())
   156  	}
   157  
   158  	var progStr, auxInfo string
   159  	if len(layers) > 0 {
   160  		render := strings.Join(completed, "")
   161  		prefix := f.pullCompletedStyle.Render(fmt.Sprintf("%d Layers", len(layers)))
   162  		auxInfo = f.auxInfoStyle.Render(fmt.Sprintf("[%s / %s]", humanize.Bytes(current), humanize.Bytes(size)))
   163  		if len(layers) == numCompleted {
   164  			auxInfo = f.auxInfoStyle.Render(fmt.Sprintf("[%s] Extracting...", humanize.Bytes(size)))
   165  		}
   166  
   167  		progStr = fmt.Sprintf("%s%s%s%s", prefix, f.layerCap(false), render, f.layerCap(true))
   168  	}
   169  
   170  	return progStr + auxInfo
   171  }
   172  
   173  // formatPullPhase returns a single character that represents the status of a layer pull.
   174  func (f containerdPullStatusFormatter) formatPullPhase(completed bool, inputStr string) string {
   175  	if completed {
   176  		return f.pullCompletedStyle.Render(f.pullStageChars[len(f.pullStageChars)-1])
   177  	}
   178  	return f.pullDownloadStyle.Render(inputStr)
   179  }
   180  
   181  func (f containerdPullStatusFormatter) layerCap(end bool) string {
   182  	l := len(f.layerCaps)
   183  	if l == 0 {
   184  		return ""
   185  	}
   186  	if end {
   187  		return f.layerCaps[l-1]
   188  	}
   189  	return f.layerCaps[0]
   190  }