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 }