Files
helmfile/pkg/kubedog/display.go
T
yxxheroandcopilot-swe-agent[bot] 27015e8d53 fix: restore kubedog status progress output during tracking (#2602)
* fix: restore kubedog status progress output during tracking

The refactor in commit bda57b74 that replaced multitrack.Multitrack() with
individual resource trackers only read from Ready/Failed/Succeeded channels,
ignoring Status, Added, EventMsg, PodLogChunk, PodError, and AddedPod channels.
This caused kubedog status messages to no longer be displayed.

Additionally, IgnoreLogs was not passed to tracker.Options, so the trackLogs
setting was effectively ignored.

This fix restores the original multitrack-style table display using the same
kubedog utils.Table and indicators packages for:
- Formatted status tables with DEPLOYMENT/REPLICAS/AVAILABLE/UP-TO-DATE columns
- Pod sub-tables showing POD/READY/RESTARTS/STATUS with tree structure
- ANSI color coding (green=ready, yellow=in-progress, red=failed)
- Progress indicators showing value transitions (e.g. 1->3)
- Waiting messages in blue

Fixes #2601

Signed-off-by: yxxhero <aiopsclub@163.com>

* fix: address review feedback - caption coloring, termWidth, O(1) pod detection, display tests

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/147fc763-c3f2-4a7e-9591-6f972fb62667

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* fix: use status.FailedReason for canary final display, fix test name typo

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/147fc763-c3f2-4a7e-9591-6f972fb62667

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* fix: correct gci import grouping in display.go and display_test.go

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/7e8f8219-5979-44fb-9729-6138c3aae08b

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

* fix: force ANSI color output in display_test.go for CI non-TTY environments

Agent-Logs-Url: https://github.com/helmfile/helmfile/sessions/ff37ccd9-f4d1-4d42-a7d0-4903e2b9d253

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>

---------

Signed-off-by: yxxhero <aiopsclub@163.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-05-20 20:53:03 +08:00

313 lines
9.1 KiB
Go

package kubedog
import (
"fmt"
"io"
"os"
"sort"
"strings"
"github.com/werf/kubedog/pkg/tracker/daemonset"
"github.com/werf/kubedog/pkg/tracker/deployment"
"github.com/werf/kubedog/pkg/tracker/indicators"
"github.com/werf/kubedog/pkg/tracker/job"
"github.com/werf/kubedog/pkg/tracker/pod"
"github.com/werf/kubedog/pkg/tracker/statefulset"
"github.com/werf/kubedog/pkg/utils"
"golang.org/x/term"
)
var statusProgressTableRatio = []float64{.58, .11, .12, .19}
var statusProgressSubTableRatio = []float64{.40, .15, .20, .25}
func writeOut(out io.Writer, s string) {
_, _ = fmt.Fprint(out, s)
}
func displayDeploymentStatusProgress(out io.Writer, resourceCaption string, status deployment.DeploymentStatus, prevStatus *deployment.DeploymentStatus) {
t := utils.NewTable(statusProgressTableRatio...)
t.SetWidth(termWidth())
showProgress := status.StatusGeneration > prevStatus.StatusGeneration
replicas := "-"
if status.ReplicasIndicator != nil {
replicas = status.ReplicasIndicator.FormatTableElem(prevStatus.ReplicasIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
WithTargetValue: true,
})
}
available := "-"
if status.AvailableIndicator != nil {
available = status.AvailableIndicator.FormatTableElem(prevStatus.AvailableIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
})
}
uptodate := "-"
if status.UpToDateIndicator != nil {
uptodate = status.UpToDateIndicator.FormatTableElem(prevStatus.UpToDateIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
})
}
t.Header("DEPLOYMENT", "REPLICAS", "AVAILABLE", "UP-TO-DATE")
args := []interface{}{resourceCaption, replicas, available, uptodate}
if status.IsFailed {
args = append(args, formatResourceError(status.FailedReason))
}
t.Row(args...)
displayChildPodsAndWaiting(&t, prevStatus.Pods, status.Pods, status.NewPodsNames, status.WaitingForMessages)
writeOut(out, t.Render())
}
func displayStatefulSetStatusProgress(out io.Writer, resourceCaption string, status statefulset.StatefulSetStatus, prevStatus *statefulset.StatefulSetStatus) {
t := utils.NewTable(statusProgressTableRatio...)
t.SetWidth(termWidth())
showProgress := status.StatusGeneration > prevStatus.StatusGeneration
replicas := "-"
if status.ReplicasIndicator != nil {
replicas = status.ReplicasIndicator.FormatTableElem(prevStatus.ReplicasIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
WithTargetValue: true,
})
}
ready := "-"
if status.ReadyIndicator != nil {
ready = status.ReadyIndicator.FormatTableElem(prevStatus.ReadyIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
})
}
uptodate := "-"
if status.UpToDateIndicator != nil {
uptodate = status.UpToDateIndicator.FormatTableElem(prevStatus.UpToDateIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
})
}
t.Header("STATEFULSET", "REPLICAS", "READY", "UP-TO-DATE")
args := []interface{}{resourceCaption, replicas, ready, uptodate}
if status.IsFailed {
args = append(args, formatResourceError(status.FailedReason))
} else {
for _, w := range status.WarningMessages {
args = append(args, formatResourceWarning(w))
}
}
t.Row(args...)
displayChildPodsAndWaiting(&t, prevStatus.Pods, status.Pods, status.NewPodsNames, status.WaitingForMessages)
writeOut(out, t.Render())
}
func displayDaemonSetStatusProgress(out io.Writer, resourceCaption string, status daemonset.DaemonSetStatus, prevStatus *daemonset.DaemonSetStatus) {
t := utils.NewTable(statusProgressTableRatio...)
t.SetWidth(termWidth())
showProgress := status.StatusGeneration > prevStatus.StatusGeneration
replicas := "-"
if status.ReplicasIndicator != nil {
replicas = status.ReplicasIndicator.FormatTableElem(prevStatus.ReplicasIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
WithTargetValue: true,
})
}
available := "-"
if status.AvailableIndicator != nil {
available = status.AvailableIndicator.FormatTableElem(prevStatus.AvailableIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
})
}
uptodate := "-"
if status.UpToDateIndicator != nil {
uptodate = status.UpToDateIndicator.FormatTableElem(prevStatus.UpToDateIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
})
}
t.Header("DAEMONSET", "REPLICAS", "AVAILABLE", "UP-TO-DATE")
args := []interface{}{resourceCaption, replicas, available, uptodate}
if status.IsFailed {
args = append(args, formatResourceError(status.FailedReason))
}
t.Row(args...)
displayChildPodsAndWaiting(&t, prevStatus.Pods, status.Pods, status.NewPodsNames, status.WaitingForMessages)
writeOut(out, t.Render())
}
func displayJobStatusProgress(out io.Writer, resourceCaption string, status job.JobStatus, prevStatus *job.JobStatus) {
t := utils.NewTable(statusProgressTableRatio...)
t.SetWidth(termWidth())
showProgress := status.StatusGeneration > prevStatus.StatusGeneration
succeeded := "-"
if status.SucceededIndicator != nil {
succeeded = status.SucceededIndicator.FormatTableElem(prevStatus.SucceededIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
})
}
t.Header("JOB", "ACTIVE", "DURATION", "SUCCEEDED/FAILED")
var active interface{} = "-"
if status.Active != 0 {
active = status.Active
}
failed := fmt.Sprintf("%d", status.Failed)
args := []interface{}{resourceCaption, active, status.Age, strings.Join([]string{succeeded, failed}, "/")}
if status.IsFailed {
args = append(args, formatResourceError(status.FailedReason))
}
t.Row(args...)
if len(status.Pods) > 0 {
st := displayChildPodsStatusProgress(&t, prevStatus.Pods, status.Pods, nil, showProgress)
extraMsg := ""
if len(status.WaitingForMessages) > 0 {
extraMsg += "---\n"
extraMsg += utils.BlueF("Waiting for: %s", strings.Join(status.WaitingForMessages, ", "))
}
st.Commit(extraMsg)
}
writeOut(out, t.Render())
}
func displayChildPodsAndWaiting(t *utils.Table, prevPods, pods map[string]pod.PodStatus, newPodsNames []string, waitingForMessages []string) {
if len(pods) > 0 {
st := displayChildPodsStatusProgress(t, prevPods, pods, newPodsNames, true)
extraMsg := ""
if len(waitingForMessages) > 0 {
extraMsg += "---\n"
extraMsg += utils.BlueF("Waiting for: %s", strings.Join(waitingForMessages, ", "))
}
st.Commit(extraMsg)
}
}
func displayChildPodsStatusProgress(t *utils.Table, prevPods, pods map[string]pod.PodStatus, newPodsNames []string, showProgress bool) *utils.Table {
subT := t.SubTable(statusProgressSubTableRatio...)
st := &subT
st.Header("POD", "READY", "RESTARTS", "STATUS")
podsNames := make([]string, 0, len(pods))
for podName := range pods {
podsNames = append(podsNames, podName)
}
sort.Strings(podsNames)
var podRows [][]interface{}
newPodSet := make(map[string]struct{}, len(newPodsNames))
for _, name := range newPodsNames {
newPodSet[name] = struct{}{}
}
for _, podName := range podsNames {
var podRow []interface{}
_, isPodNew := newPodSet[podName]
prevPodStatus := prevPods[podName]
podStatus := pods[podName]
isReady := false
if podStatus.StatusIndicator != nil {
isReady = podStatus.StatusIndicator.IsReady()
}
resource := formatPodResourceCaption(podName, isReady, podStatus.IsFailed, isPodNew)
ready := fmt.Sprintf("%d/%d", podStatus.ReadyContainers, podStatus.TotalContainers)
status := "-"
if podStatus.StatusIndicator != nil {
status = podStatus.StatusIndicator.FormatTableElem(prevPodStatus.StatusIndicator, indicators.FormatTableElemOptions{
ShowProgress: showProgress,
IsResourceNew: isPodNew,
})
}
podRow = append(podRow, resource, ready, podStatus.Restarts, status)
if podStatus.IsFailed {
podRow = append(podRow, formatResourceError(podStatus.FailedReason))
}
podRows = append(podRows, podRow)
}
st.Rows(podRows...)
return st
}
func formatResourceCaption(caption string, isReady, isFailed bool) string {
switch {
case isReady:
return utils.GreenF("%s", caption)
case isFailed:
return utils.RedF("%s", caption)
default:
return utils.YellowF("%s", caption)
}
}
func formatPodResourceCaption(podName string, isReady, isFailed, isNew bool) string {
if !isNew {
return podName
}
return formatResourceCaption(podName, isReady, isFailed)
}
func formatResourceError(reason string) string {
return utils.RedF("error: %s", reason)
}
func formatResourceWarning(reason string) string {
return utils.YellowF("warning: %s", reason)
}
func termWidth() int {
if w, _, err := term.GetSize(int(os.Stderr.Fd())); err == nil && w > 0 {
return w
}
return 140
}
func displayCanaryStatus(out io.Writer, resourceCaption string, status CanaryStatusView) {
var parts []string
if status.Phase != "" {
parts = append(parts, fmt.Sprintf("phase %s", status.Phase))
}
if status.Age != "" {
parts = append(parts, fmt.Sprintf("age %s", status.Age))
}
msg := fmt.Sprintf("%s: %s", resourceCaption, strings.Join(parts, ", "))
if status.IsFailed {
msg = utils.RedF("%s", msg)
}
_, _ = fmt.Fprintln(out, msg)
}
type CanaryStatusView struct {
Phase string
Age string
IsFailed bool
}
func statusOutput() io.Writer {
return os.Stderr
}