blob: 5c7839ab8deaefde62d2cc586ba962f5f6dae414 [file] [log] [blame]
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package progress
import (
"fmt"
"io"
"sort"
"strings"
"sync"
)
import (
"github.com/cheggaaa/pb/v3"
)
import (
"github.com/apache/dubbo-go-pixiu/operator/pkg/name"
)
type InstallState int
const (
StateInstalling InstallState = iota
StatePruning
StateComplete
StateUninstallComplete
)
// Log records the progress of an installation
// This aims to provide information about the install of multiple components in parallel, while working
// around the limitations of the pb library, which will only support single lines. To do this, we aggregate
// the current components into a single line, and as components complete there final state is persisted to a new line.
type Log struct {
components map[string]*ManifestLog
bar *pb.ProgressBar
template string
mu sync.Mutex
state InstallState
}
func NewLog() *Log {
return &Log{
components: map[string]*ManifestLog{},
bar: createBar(),
}
}
const inProgress = `{{ yellow (cycle . "-" "-" "-" " ") }} `
// createStatus will return a string to report the current status.
// ex: - Processing resources for components. Waiting for foo, bar
func (p *Log) createStatus(maxWidth int) string {
comps := make([]string, 0, len(p.components))
wait := make([]string, 0, len(p.components))
for c, l := range p.components {
comps = append(comps, name.UserFacingComponentName(name.ComponentName(c)))
wait = append(wait, l.waitingResources()...)
}
sort.Strings(comps)
sort.Strings(wait)
msg := fmt.Sprintf(`Processing resources for %s.`, strings.Join(comps, ", "))
if len(wait) > 0 {
msg += fmt.Sprintf(` Waiting for %s`, strings.Join(wait, ", "))
}
prefix := inProgress
if !p.bar.GetBool(pb.Terminal) {
// If we aren't a terminal, no need to spam extra lines
prefix = `{{ yellow "-" }} `
}
// reduce by 2 to allow for the "- " that will be added below
maxWidth -= 2
if maxWidth > 0 && len(msg) > maxWidth {
return prefix + msg[:maxWidth-3] + "..."
}
// cycle will alternate between "-" and " ". "-" is given multiple times to avoid quick flashing back and forth
return prefix + msg
}
// For testing only
var testWriter *io.Writer
func createBar() *pb.ProgressBar {
// Don't set a total and use Static so we can explicitly control when you write. This is needed
// for handling the multiline issues.
bar := pb.New(0)
bar.Set(pb.Static, true)
if testWriter != nil {
bar.SetWriter(*testWriter)
}
bar.Start()
// if we aren't a terminal, we will return a new line for each new message
if !bar.GetBool(pb.Terminal) {
bar.Set(pb.ReturnSymbol, "\n")
}
return bar
}
// reportProgress will report an update for a given component
// Because the bar library does not support multiple lines/bars at once, we need to aggregate current
// progress into a single line. For example "Waiting for x, y, z". Once a component completes, we want
// a new line created so the information is not lost. To do this, we spin up a new bar with the remaining components
// on a new line, and create a new bar. For example, this becomes "x succeeded", "waiting for y, z".
func (p *Log) reportProgress(component string) func() {
return func() {
cliName := name.UserFacingComponentName(name.ComponentName(component))
p.mu.Lock()
defer p.mu.Unlock()
cmp := p.components[component]
// The component has completed
cmp.mu.Lock()
finished := cmp.finished
cmpErr := cmp.err
cmp.mu.Unlock()
if finished || cmpErr != "" {
if finished {
p.SetMessage(fmt.Sprintf(`{{ green "✔" }} %s installed`, cliName), true)
} else {
p.SetMessage(fmt.Sprintf(`{{ red "✘" }} %s encountered an error: %s`, cliName, cmpErr), true)
}
// Close the bar out, outputting a new line
delete(p.components, component)
// Now we create a new bar, which will have the remaining components
p.bar = createBar()
return
}
p.SetMessage(p.createStatus(p.bar.Width()), false)
}
}
func (p *Log) SetState(state InstallState) {
p.mu.Lock()
defer p.mu.Unlock()
p.state = state
switch p.state {
case StatePruning:
p.bar.SetTemplateString(inProgress + `Pruning removed resources`)
p.bar.Write()
case StateComplete:
p.bar.SetTemplateString(`{{ green "✔" }} Installation complete`)
p.bar.Write()
case StateUninstallComplete:
p.bar.SetTemplateString(`{{ green "✔" }} Uninstall complete`)
p.bar.Write()
}
}
func (p *Log) NewComponent(component string) *ManifestLog {
ml := &ManifestLog{
report: p.reportProgress(component),
}
p.mu.Lock()
defer p.mu.Unlock()
p.components[component] = ml
return ml
}
func (p *Log) SetMessage(status string, finish bool) {
// if we are not a terminal and there is no change, do not write
// This avoids redundant lines
if !p.bar.GetBool(pb.Terminal) && status == p.template {
return
}
p.template = status
p.bar.SetTemplateString(p.template)
if finish {
p.bar.Finish()
}
p.bar.Write()
}
// ManifestLog records progress for a single component
type ManifestLog struct {
report func()
err string
finished bool
waiting []string
mu sync.Mutex
}
func (p *ManifestLog) ReportProgress() {
if p == nil {
return
}
p.report()
}
func (p *ManifestLog) ReportError(err string) {
if p == nil {
return
}
p.mu.Lock()
p.err = err
p.mu.Unlock()
p.report()
}
func (p *ManifestLog) ReportFinished() {
if p == nil {
return
}
p.mu.Lock()
p.finished = true
p.mu.Unlock()
p.report()
}
func (p *ManifestLog) ReportWaiting(resources []string) {
if p == nil {
return
}
p.mu.Lock()
p.waiting = resources
p.mu.Unlock()
p.report()
}
func (p *ManifestLog) waitingResources() []string {
p.mu.Lock()
defer p.mu.Unlock()
return p.waiting
}