istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/util/progress/progress.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package progress 16 17 import ( 18 "fmt" 19 "io" 20 "sort" 21 "strings" 22 "sync" 23 24 "github.com/cheggaaa/pb/v3" 25 26 "istio.io/istio/operator/pkg/name" 27 ) 28 29 type InstallState int 30 31 const ( 32 StateInstalling InstallState = iota 33 StatePruning 34 StateComplete 35 StateUninstallComplete 36 ) 37 38 // Log records the progress of an installation 39 // This aims to provide information about the install of multiple components in parallel, while working 40 // around the limitations of the pb library, which will only support single lines. To do this, we aggregate 41 // the current components into a single line, and as components complete there final state is persisted to a new line. 42 type Log struct { 43 components map[string]*ManifestLog 44 bar *pb.ProgressBar 45 template string 46 mu sync.Mutex 47 state InstallState 48 } 49 50 func NewLog() *Log { 51 return &Log{ 52 components: map[string]*ManifestLog{}, 53 bar: createBar(), 54 } 55 } 56 57 const inProgress = `{{ yellow (cycle . "-" "-" "-" " ") }} ` 58 59 // createStatus will return a string to report the current status. 60 // ex: - Processing resources for components. Waiting for foo, bar 61 func (p *Log) createStatus(maxWidth int) string { 62 comps := make([]string, 0, len(p.components)) 63 wait := make([]string, 0, len(p.components)) 64 for c, l := range p.components { 65 comps = append(comps, name.UserFacingComponentName(name.ComponentName(c))) 66 wait = append(wait, l.waitingResources()...) 67 } 68 sort.Strings(comps) 69 sort.Strings(wait) 70 msg := fmt.Sprintf(`Processing resources for %s.`, strings.Join(comps, ", ")) 71 if len(wait) > 0 { 72 msg += fmt.Sprintf(` Waiting for %s`, strings.Join(wait, ", ")) 73 } 74 prefix := inProgress 75 if !p.bar.GetBool(pb.Terminal) { 76 // If we aren't a terminal, no need to spam extra lines 77 prefix = `{{ yellow "-" }} ` 78 } 79 // reduce by 2 to allow for the "- " that will be added below 80 maxWidth -= 2 81 if maxWidth > 0 && len(msg) > maxWidth { 82 return prefix + msg[:maxWidth-3] + "..." 83 } 84 // cycle will alternate between "-" and " ". "-" is given multiple times to avoid quick flashing back and forth 85 return prefix + msg 86 } 87 88 // For testing only 89 var testWriter *io.Writer 90 91 func createBar() *pb.ProgressBar { 92 // Don't set a total and use Static so we can explicitly control when you write. This is needed 93 // for handling the multiline issues. 94 bar := pb.New(0) 95 bar.Set(pb.Static, true) 96 if testWriter != nil { 97 bar.SetWriter(*testWriter) 98 } 99 bar.Start() 100 // if we aren't a terminal, we will return a new line for each new message 101 if !bar.GetBool(pb.Terminal) { 102 bar.Set(pb.ReturnSymbol, "\n") 103 } 104 return bar 105 } 106 107 // reportProgress will report an update for a given component 108 // Because the bar library does not support multiple lines/bars at once, we need to aggregate current 109 // progress into a single line. For example "Waiting for x, y, z". Once a component completes, we want 110 // a new line created so the information is not lost. To do this, we spin up a new bar with the remaining components 111 // on a new line, and create a new bar. For example, this becomes "x succeeded", "waiting for y, z". 112 func (p *Log) reportProgress(component string) func() { 113 return func() { 114 cmpName := name.ComponentName(component) 115 cliName := name.UserFacingComponentName(cmpName) 116 p.mu.Lock() 117 defer p.mu.Unlock() 118 cmp := p.components[component] 119 // The component has completed 120 cmp.mu.Lock() 121 finished := cmp.finished 122 cmpErr := cmp.err 123 cmp.mu.Unlock() 124 successIcon := "✅" 125 if icon, found := name.IstioComponentSuccessIcons[cmpName]; found { 126 successIcon = icon 127 } 128 if finished || cmpErr != "" { 129 if finished { 130 p.SetMessage(fmt.Sprintf(`%s %s installed`, successIcon, cliName), true) 131 } else { 132 p.SetMessage(fmt.Sprintf(`❌ %s encountered an error: %s`, cliName, cmpErr), true) 133 } 134 // Close the bar out, outputting a new line 135 delete(p.components, component) 136 137 // Now we create a new bar, which will have the remaining components 138 p.bar = createBar() 139 return 140 } 141 p.SetMessage(p.createStatus(p.bar.Width()), false) 142 } 143 } 144 145 func (p *Log) SetState(state InstallState) { 146 p.mu.Lock() 147 defer p.mu.Unlock() 148 p.state = state 149 switch p.state { 150 case StatePruning: 151 p.bar.SetTemplateString(inProgress + `Pruning removed resources`) 152 p.bar.Write() 153 case StateComplete: 154 p.bar.SetTemplateString(`{{ green "✅" }} Installation complete`) 155 p.bar.Write() 156 case StateUninstallComplete: 157 p.bar.SetTemplateString(`{{ green "✅" }} Uninstall complete`) 158 p.bar.Write() 159 } 160 } 161 162 func (p *Log) NewComponent(component string) *ManifestLog { 163 ml := &ManifestLog{ 164 report: p.reportProgress(component), 165 } 166 p.mu.Lock() 167 defer p.mu.Unlock() 168 p.components[component] = ml 169 return ml 170 } 171 172 func (p *Log) SetMessage(status string, finish bool) { 173 // if we are not a terminal and there is no change, do not write 174 // This avoids redundant lines 175 if !p.bar.GetBool(pb.Terminal) && status == p.template { 176 return 177 } 178 p.template = status 179 p.bar.SetTemplateString(p.template) 180 if finish { 181 p.bar.Finish() 182 } 183 p.bar.Write() 184 } 185 186 // ManifestLog records progress for a single component 187 type ManifestLog struct { 188 report func() 189 err string 190 finished bool 191 waiting []string 192 mu sync.Mutex 193 } 194 195 func (p *ManifestLog) ReportProgress() { 196 if p == nil { 197 return 198 } 199 p.report() 200 } 201 202 func (p *ManifestLog) ReportError(err string) { 203 if p == nil { 204 return 205 } 206 p.mu.Lock() 207 p.err = err 208 p.mu.Unlock() 209 p.report() 210 } 211 212 func (p *ManifestLog) ReportFinished() { 213 if p == nil { 214 return 215 } 216 p.mu.Lock() 217 p.finished = true 218 p.mu.Unlock() 219 p.report() 220 } 221 222 func (p *ManifestLog) ReportWaiting(resources []string) { 223 if p == nil { 224 return 225 } 226 p.mu.Lock() 227 p.waiting = resources 228 p.mu.Unlock() 229 p.report() 230 } 231 232 func (p *ManifestLog) waitingResources() []string { 233 p.mu.Lock() 234 defer p.mu.Unlock() 235 return p.waiting 236 }