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 }