github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/cmd/syft/cli/ui/handle_pull_docker_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/docker"
    16  	"github.com/anchore/syft/internal/log"
    17  )
    18  
    19  var _ interface {
    20  	progress.Stager
    21  	progress.Progressable
    22  } = (*dockerPullProgressAdapter)(nil)
    23  
    24  type dockerPullStatus interface {
    25  	Complete() bool
    26  	Layers() []docker.LayerID
    27  	Current(docker.LayerID) docker.LayerState
    28  }
    29  
    30  type dockerPullProgressAdapter struct {
    31  	status    dockerPullStatus
    32  	formatter dockerPullStatusFormatter
    33  }
    34  
    35  type dockerPullStatusFormatter struct {
    36  	auxInfoStyle             lipgloss.Style
    37  	dockerPullCompletedStyle lipgloss.Style
    38  	dockerPullDownloadStyle  lipgloss.Style
    39  	dockerPullExtractStyle   lipgloss.Style
    40  	dockerPullStageChars     []string
    41  	layerCaps                []string
    42  }
    43  
    44  func (m *Handler) handlePullDockerImage(e partybus.Event) []tea.Model {
    45  	_, pullStatus, err := stereoscopeParsers.ParsePullDockerImage(e)
    46  	if err != nil {
    47  		log.WithFields("error", err).Warn("unable to parse event")
    48  		return nil
    49  	}
    50  
    51  	tsk := m.newTaskProgress(
    52  		taskprogress.Title{
    53  			Default: "Pull image",
    54  			Running: "Pulling image",
    55  			Success: "Pulled image",
    56  		},
    57  		taskprogress.WithStagedProgressable(
    58  			newDockerPullProgressAdapter(pullStatus),
    59  		),
    60  	)
    61  
    62  	tsk.HintStyle = lipgloss.NewStyle()
    63  	tsk.HintEndCaps = nil
    64  
    65  	return []tea.Model{tsk}
    66  }
    67  
    68  func newDockerPullProgressAdapter(status dockerPullStatus) *dockerPullProgressAdapter {
    69  	return &dockerPullProgressAdapter{
    70  		status:    status,
    71  		formatter: newDockerPullStatusFormatter(),
    72  	}
    73  }
    74  
    75  func newDockerPullStatusFormatter() dockerPullStatusFormatter {
    76  	return dockerPullStatusFormatter{
    77  		auxInfoStyle:             lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")),
    78  		dockerPullCompletedStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("#fcba03")),
    79  		dockerPullDownloadStyle:  lipgloss.NewStyle().Foreground(lipgloss.Color("#777777")),
    80  		dockerPullExtractStyle:   lipgloss.NewStyle().Foreground(lipgloss.Color("#ffffff")),
    81  		dockerPullStageChars:     strings.Split("▁▃▄▅▆▇█", ""),
    82  		layerCaps:                strings.Split("▕▏", ""),
    83  	}
    84  }
    85  
    86  func (d dockerPullProgressAdapter) Size() int64 {
    87  	return -1
    88  }
    89  
    90  func (d dockerPullProgressAdapter) Current() int64 {
    91  	return 1
    92  }
    93  
    94  func (d dockerPullProgressAdapter) Error() error {
    95  	if d.status.Complete() {
    96  		return progress.ErrCompleted
    97  	}
    98  	// TODO: return intermediate error indications
    99  	return nil
   100  }
   101  
   102  func (d dockerPullProgressAdapter) Stage() string {
   103  	return d.formatter.Render(d.status)
   104  }
   105  
   106  // Render crafts the given docker image pull status summarized into a single line.
   107  func (f dockerPullStatusFormatter) Render(pullStatus dockerPullStatus) string {
   108  	var size, current uint64
   109  
   110  	layers := pullStatus.Layers()
   111  	status := make(map[docker.LayerID]docker.LayerState)
   112  	completed := make([]string, len(layers))
   113  
   114  	// fetch the current state
   115  	for idx, layer := range layers {
   116  		completed[idx] = " "
   117  		status[layer] = pullStatus.Current(layer)
   118  	}
   119  
   120  	numCompleted := 0
   121  	for idx, layer := range layers {
   122  		prog := status[layer].PhaseProgress
   123  		curN := prog.Current()
   124  		curSize := prog.Size()
   125  
   126  		if progress.IsCompleted(prog) {
   127  			input := f.dockerPullStageChars[len(f.dockerPullStageChars)-1]
   128  			completed[idx] = f.formatDockerPullPhase(status[layer].Phase, input)
   129  		} else if curN != 0 {
   130  			var ratio float64
   131  			switch {
   132  			case curN == 0 || curSize < 0:
   133  				ratio = 0
   134  			case curN >= curSize:
   135  				ratio = 1
   136  			default:
   137  				ratio = float64(curN) / float64(curSize)
   138  			}
   139  
   140  			i := int(ratio * float64(len(f.dockerPullStageChars)-1))
   141  			input := f.dockerPullStageChars[i]
   142  			completed[idx] = f.formatDockerPullPhase(status[layer].Phase, input)
   143  		}
   144  
   145  		if progress.IsErrCompleted(status[layer].DownloadProgress.Error()) {
   146  			numCompleted++
   147  		}
   148  	}
   149  
   150  	for _, layer := range layers {
   151  		prog := status[layer].DownloadProgress
   152  		size += uint64(prog.Size())
   153  		current += uint64(prog.Current())
   154  	}
   155  
   156  	var progStr, auxInfo string
   157  	if len(layers) > 0 {
   158  		render := strings.Join(completed, "")
   159  		prefix := f.dockerPullCompletedStyle.Render(fmt.Sprintf("%d Layers", len(layers)))
   160  		auxInfo = f.auxInfoStyle.Render(fmt.Sprintf("[%s / %s]", humanize.Bytes(current), humanize.Bytes(size)))
   161  		if len(layers) == numCompleted {
   162  			auxInfo = f.auxInfoStyle.Render(fmt.Sprintf("[%s] Extracting...", humanize.Bytes(size)))
   163  		}
   164  
   165  		progStr = fmt.Sprintf("%s%s%s%s", prefix, f.layerCap(false), render, f.layerCap(true))
   166  	}
   167  
   168  	return progStr + auxInfo
   169  }
   170  
   171  // formatDockerPullPhase returns a single character that represents the status of a layer pull.
   172  func (f dockerPullStatusFormatter) formatDockerPullPhase(phase docker.PullPhase, inputStr string) string {
   173  	switch phase {
   174  	case docker.WaitingPhase:
   175  		// ignore any progress related to waiting
   176  		return " "
   177  	case docker.PullingFsPhase, docker.DownloadingPhase:
   178  		return f.dockerPullDownloadStyle.Render(inputStr)
   179  	case docker.DownloadCompletePhase:
   180  		return f.dockerPullDownloadStyle.Render(f.dockerPullStageChars[len(f.dockerPullStageChars)-1])
   181  	case docker.ExtractingPhase:
   182  		return f.dockerPullExtractStyle.Render(inputStr)
   183  	case docker.VerifyingChecksumPhase, docker.PullCompletePhase:
   184  		return f.dockerPullCompletedStyle.Render(inputStr)
   185  	case docker.AlreadyExistsPhase:
   186  		return f.dockerPullCompletedStyle.Render(f.dockerPullStageChars[len(f.dockerPullStageChars)-1])
   187  	default:
   188  		return inputStr
   189  	}
   190  }
   191  
   192  func (f dockerPullStatusFormatter) layerCap(end bool) string {
   193  	l := len(f.layerCaps)
   194  	if l == 0 {
   195  		return ""
   196  	}
   197  	if end {
   198  		return f.layerCaps[l-1]
   199  	}
   200  	return f.layerCaps[0]
   201  }