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>
This commit is contained in:
yxxhero
2026-05-20 20:53:03 +08:00
committed by GitHub
co-authored by copilot-swe-agent[bot]
parent 781d28a47a
commit 27015e8d53
4 changed files with 894 additions and 19 deletions
+312
View File
@@ -0,0 +1,312 @@
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
}
+453
View File
@@ -0,0 +1,453 @@
package kubedog
import (
"bytes"
"os"
"strings"
"testing"
"github.com/gookit/color"
"github.com/stretchr/testify/assert"
"github.com/werf/kubedog/pkg/tracker/daemonset"
"github.com/werf/kubedog/pkg/tracker/deployment"
"github.com/werf/kubedog/pkg/tracker/job"
"github.com/werf/kubedog/pkg/tracker/pod"
"github.com/werf/kubedog/pkg/tracker/statefulset"
)
// TestMain forces ANSI color output so that tests asserting on escape codes
// pass in non-TTY environments such as CI runners.
func TestMain(m *testing.M) {
color.ForceColor()
os.Exit(m.Run())
}
// --- formatResourceCaption ---
func TestFormatResourceCaption_Ready(t *testing.T) {
result := formatResourceCaption("deploy/myapp", true, false)
assert.Contains(t, result, "deploy/myapp")
// Green ANSI escape should be present
assert.Contains(t, result, "\033[")
}
func TestFormatResourceCaption_Failed(t *testing.T) {
result := formatResourceCaption("deploy/myapp", false, true)
assert.Contains(t, result, "deploy/myapp")
assert.Contains(t, result, "\033[")
}
func TestFormatResourceCaption_InProgress(t *testing.T) {
result := formatResourceCaption("deploy/myapp", false, false)
assert.Contains(t, result, "deploy/myapp")
// Yellow for in-progress
assert.Contains(t, result, "\033[")
}
func TestFormatResourceCaption_ReadyTakesPrecedence(t *testing.T) {
// isReady=true should win over isFailed=true
resultReady := formatResourceCaption("x", true, false)
resultFailed := formatResourceCaption("x", false, true)
// Colors should differ
assert.NotEqual(t, resultReady, resultFailed)
}
// --- formatPodResourceCaption ---
func TestFormatPodResourceCaption_NotNew(t *testing.T) {
result := formatPodResourceCaption("my-pod-abc", true, false, false)
// Not a new pod: no coloring applied, just the plain name
assert.Equal(t, "my-pod-abc", result)
}
func TestFormatPodResourceCaption_NewAndReady(t *testing.T) {
result := formatPodResourceCaption("my-pod-abc", true, false, true)
assert.Contains(t, result, "my-pod-abc")
assert.Contains(t, result, "\033[")
}
func TestFormatPodResourceCaption_NewAndFailed(t *testing.T) {
result := formatPodResourceCaption("my-pod-abc", false, true, true)
assert.Contains(t, result, "my-pod-abc")
assert.Contains(t, result, "\033[")
}
func TestFormatPodResourceCaption_NewInProgress(t *testing.T) {
result := formatPodResourceCaption("my-pod-abc", false, false, true)
assert.Contains(t, result, "my-pod-abc")
assert.Contains(t, result, "\033[")
}
// --- formatResourceError / formatResourceWarning ---
func TestFormatResourceError(t *testing.T) {
result := formatResourceError("CrashLoopBackOff")
assert.Contains(t, result, "error:")
assert.Contains(t, result, "CrashLoopBackOff")
}
func TestFormatResourceWarning(t *testing.T) {
result := formatResourceWarning("PodNotScheduled")
assert.Contains(t, result, "warning:")
assert.Contains(t, result, "PodNotScheduled")
}
// --- termWidth ---
func TestTermWidth_ReturnsPositive(t *testing.T) {
w := termWidth()
assert.Greater(t, w, 0)
}
// --- displayDeploymentStatusProgress ---
func TestDisplayDeploymentStatusProgress_ZeroStatus(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("deploy/myapp", false, false)
var prev deployment.DeploymentStatus
status := deployment.DeploymentStatus{}
// Must not panic and must produce some output
assert.NotPanics(t, func() {
displayDeploymentStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.NotEmpty(t, out)
assert.Contains(t, out, "DEPLOYMENT")
}
func TestDisplayDeploymentStatusProgress_Failed(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("deploy/myapp", false, true)
var prev deployment.DeploymentStatus
status := deployment.DeploymentStatus{
IsFailed: true,
FailedReason: "ImagePullBackOff",
}
assert.NotPanics(t, func() {
displayDeploymentStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "error:")
assert.Contains(t, out, "ImagePullBackOff")
}
func TestDisplayDeploymentStatusProgress_WithWaitingMessage(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("deploy/myapp", false, false)
var prev deployment.DeploymentStatus
// WaitingForMessages is only rendered when there are pods
status := deployment.DeploymentStatus{
StatusGeneration: 1,
WaitingForMessages: []string{"up-to-date 1->3"},
Pods: map[string]pod.PodStatus{
"myapp-pod-abc": {ReadyContainers: 1, TotalContainers: 1},
},
}
assert.NotPanics(t, func() {
displayDeploymentStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "Waiting for:")
assert.Contains(t, out, "up-to-date 1->3")
}
func TestDisplayDeploymentStatusProgress_WithPods(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("deploy/myapp", false, false)
prev := deployment.DeploymentStatus{}
status := deployment.DeploymentStatus{
StatusGeneration: 1,
Pods: map[string]pod.PodStatus{
"myapp-abc-123": {ReadyContainers: 1, TotalContainers: 1},
},
NewPodsNames: []string{"myapp-abc-123"},
}
assert.NotPanics(t, func() {
displayDeploymentStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "POD")
assert.Contains(t, out, "myapp-abc-123")
}
// --- displayStatefulSetStatusProgress ---
func TestDisplayStatefulSetStatusProgress_ZeroStatus(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("sts/myapp", false, false)
var prev statefulset.StatefulSetStatus
status := statefulset.StatefulSetStatus{}
assert.NotPanics(t, func() {
displayStatefulSetStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "STATEFULSET")
}
func TestDisplayStatefulSetStatusProgress_WithWarnings(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("sts/myapp", false, false)
var prev statefulset.StatefulSetStatus
status := statefulset.StatefulSetStatus{
WarningMessages: []string{"PodNotScheduled: insufficient resources"},
}
assert.NotPanics(t, func() {
displayStatefulSetStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "warning:")
assert.Contains(t, out, "PodNotScheduled")
}
func TestDisplayStatefulSetStatusProgress_Failed(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("sts/myapp", false, true)
var prev statefulset.StatefulSetStatus
status := statefulset.StatefulSetStatus{
IsFailed: true,
FailedReason: "timeout waiting for ready",
}
assert.NotPanics(t, func() {
displayStatefulSetStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "error:")
assert.Contains(t, out, "timeout waiting for ready")
}
// --- displayDaemonSetStatusProgress ---
func TestDisplayDaemonSetStatusProgress_ZeroStatus(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("ds/myapp", false, false)
var prev daemonset.DaemonSetStatus
status := daemonset.DaemonSetStatus{}
assert.NotPanics(t, func() {
displayDaemonSetStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "DAEMONSET")
}
func TestDisplayDaemonSetStatusProgress_Failed(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("ds/myapp", false, true)
var prev daemonset.DaemonSetStatus
status := daemonset.DaemonSetStatus{
IsFailed: true,
FailedReason: "node not ready",
}
assert.NotPanics(t, func() {
displayDaemonSetStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "error:")
assert.Contains(t, out, "node not ready")
}
// --- displayJobStatusProgress ---
func TestDisplayJobStatusProgress_ZeroStatus(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("job/myjob", false, false)
var prev job.JobStatus
status := job.JobStatus{}
assert.NotPanics(t, func() {
displayJobStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "JOB")
}
func TestDisplayJobStatusProgress_Active(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("job/myjob", false, false)
var prev job.JobStatus
status := job.JobStatus{
StatusGeneration: 1,
}
assert.NotPanics(t, func() {
displayJobStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "ACTIVE")
}
func TestDisplayJobStatusProgress_Failed(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("job/myjob", false, true)
var prev job.JobStatus
status := job.JobStatus{
IsFailed: true,
FailedReason: "BackoffLimitExceeded",
}
assert.NotPanics(t, func() {
displayJobStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "error:")
assert.Contains(t, out, "BackoffLimitExceeded")
}
func TestDisplayJobStatusProgress_WithWaitingMessage(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("job/myjob", false, false)
var prev job.JobStatus
status := job.JobStatus{
WaitingForMessages: []string{"succeeded 0->1"},
Pods: map[string]pod.PodStatus{
"myjob-abc": {ReadyContainers: 0, TotalContainers: 1},
},
}
assert.NotPanics(t, func() {
displayJobStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "Waiting for:")
assert.Contains(t, out, "succeeded 0->1")
}
// --- displayChildPodsStatusProgress ---
func TestDisplayChildPodsStatusProgress_Empty(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("deploy/myapp", false, false)
// With no pods, only the header should be rendered
prev := deployment.DeploymentStatus{}
status := deployment.DeploymentStatus{
Pods: map[string]pod.PodStatus{},
}
assert.NotPanics(t, func() {
displayDeploymentStatusProgress(&buf, caption, status, &prev)
})
// No POD sub-table header when pods is empty
out := buf.String()
assert.NotContains(t, out, "POD")
}
func TestDisplayChildPodsStatusProgress_NewPodSet(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("deploy/myapp", false, false)
prev := deployment.DeploymentStatus{}
// Two pods: one new, one old
status := deployment.DeploymentStatus{
StatusGeneration: 1,
Pods: map[string]pod.PodStatus{
"pod-new-abc": {ReadyContainers: 0, TotalContainers: 1},
"pod-old-xyz": {ReadyContainers: 1, TotalContainers: 1},
},
NewPodsNames: []string{"pod-new-abc"},
}
assert.NotPanics(t, func() {
displayDeploymentStatusProgress(&buf, caption, status, &prev)
})
out := buf.String()
assert.Contains(t, out, "pod-new-abc")
assert.Contains(t, out, "pod-old-xyz")
}
func TestDisplayChildPodsStatusProgress_ManyPodsO1Check(t *testing.T) {
// Verifies O(1) new-pod detection works correctly for many pods
var buf bytes.Buffer
caption := formatResourceCaption("deploy/myapp", false, false)
prev := deployment.DeploymentStatus{}
pods := make(map[string]pod.PodStatus)
newNames := make([]string, 0, 10)
for i := 0; i < 20; i++ {
name := strings.Repeat("a", i+1)
pods[name] = pod.PodStatus{ReadyContainers: 1, TotalContainers: 1}
if i%2 == 0 {
newNames = append(newNames, name)
}
}
status := deployment.DeploymentStatus{
StatusGeneration: 1,
Pods: pods,
NewPodsNames: newNames,
}
assert.NotPanics(t, func() {
displayDeploymentStatusProgress(&buf, caption, status, &prev)
})
assert.NotEmpty(t, buf.String())
}
// --- displayCanaryStatus ---
func TestDisplayCanaryStatus_Normal(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("canary/myapp", false, false)
view := CanaryStatusView{Phase: "Progressing", Age: "1m"}
assert.NotPanics(t, func() {
displayCanaryStatus(&buf, caption, view)
})
out := buf.String()
assert.Contains(t, out, "Progressing")
assert.Contains(t, out, "1m")
}
func TestDisplayCanaryStatus_Failed(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("canary/myapp", false, true)
view := CanaryStatusView{Phase: "Failed", IsFailed: true}
assert.NotPanics(t, func() {
displayCanaryStatus(&buf, caption, view)
})
out := buf.String()
assert.Contains(t, out, "Failed")
}
func TestDisplayCanaryStatus_Succeeded(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("canary/myapp", true, false)
view := CanaryStatusView{Phase: "Succeeded"}
assert.NotPanics(t, func() {
displayCanaryStatus(&buf, caption, view)
})
out := buf.String()
assert.Contains(t, out, "Succeeded")
}
func TestDisplayCanaryStatus_EmptyPhaseAndAge(t *testing.T) {
var buf bytes.Buffer
caption := formatResourceCaption("canary/myapp", false, false)
view := CanaryStatusView{}
assert.NotPanics(t, func() {
displayCanaryStatus(&buf, caption, view)
})
// Should still produce output (at least the caption + newline)
assert.NotEmpty(t, buf.String())
}
// --- writeOut ---
func TestWriteOut(t *testing.T) {
var buf bytes.Buffer
writeOut(&buf, "hello world")
assert.Equal(t, "hello world", buf.String())
}
func TestWriteOut_Empty(t *testing.T) {
var buf bytes.Buffer
writeOut(&buf, "")
assert.Equal(t, "", buf.String())
}
+129 -5
View File
@@ -9,6 +9,7 @@ import (
"sync"
"time"
"github.com/werf/kubedog/pkg/display"
"github.com/werf/kubedog/pkg/informer"
"github.com/werf/kubedog/pkg/tracker"
"github.com/werf/kubedog/pkg/tracker/canary"
@@ -234,6 +235,7 @@ func (t *Tracker) TrackResources(ctx context.Context, resources []*resource.Reso
ParentContext: ctx,
Timeout: t.trackOptions.Timeout,
LogsFromTime: time.Now().Add(-t.trackOptions.LogsSince),
IgnoreLogs: !t.trackOptions.Logs,
}
var wg sync.WaitGroup
@@ -312,13 +314,35 @@ func (t *Tracker) runDeploymentTracker(ctx context.Context, tr *deployment.Track
}
func (t *Tracker) waitDeploymentTracker(ctx context.Context, tr *deployment.Tracker, trackErrCh <-chan error, doneCh <-chan struct{}) error {
var prevStatus deployment.DeploymentStatus
out := statusOutput()
resourceName := fmt.Sprintf("deploy/%s", tr.ResourceName)
for {
select {
case status := <-tr.Added:
displayDeploymentStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
prevStatus = status
case <-tr.Ready:
t.logger.Debugf("Deployment %s/%s is ready", tr.Namespace, tr.ResourceName)
displayDeploymentStatusProgress(out, formatResourceCaption(resourceName, true, false), prevStatus, &prevStatus)
t.logger.Infof("Deployment %s/%s is ready", tr.Namespace, tr.ResourceName)
return nil
case status := <-tr.Failed:
displayDeploymentStatusProgress(out, formatResourceCaption(resourceName, false, true), status, &prevStatus)
return fmt.Errorf("deployment %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason)
case status := <-tr.Status:
if status.StatusGeneration > prevStatus.StatusGeneration {
displayDeploymentStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
prevStatus = status
}
case msg := <-tr.EventMsg:
t.logger.Infof("deploy/%s: %s", tr.ResourceName, msg)
case chunk := <-tr.PodLogChunk:
t.logPodLogChunk(chunk.PodName, chunk.LogLines)
case report := <-tr.PodError:
t.logger.Warnf("deploy/%s pod %s: %s: %s", tr.ResourceName, report.ReplicaSetPodError.PodName, report.ReplicaSetPodError.ContainerName, report.ReplicaSetPodError.Message)
case <-tr.AddedReplicaSet:
case <-tr.AddedPod:
case err := <-trackErrCh:
return err
case <-doneCh:
@@ -338,13 +362,34 @@ func (t *Tracker) runStatefulSetTracker(ctx context.Context, tr *statefulset.Tra
}
func (t *Tracker) waitStatefulSetTracker(ctx context.Context, tr *statefulset.Tracker, trackErrCh <-chan error, doneCh <-chan struct{}) error {
var prevStatus statefulset.StatefulSetStatus
out := statusOutput()
resourceName := fmt.Sprintf("sts/%s", tr.ResourceName)
for {
select {
case status := <-tr.Added:
displayStatefulSetStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
prevStatus = status
case <-tr.Ready:
t.logger.Debugf("StatefulSet %s/%s is ready", tr.Namespace, tr.ResourceName)
displayStatefulSetStatusProgress(out, formatResourceCaption(resourceName, true, false), prevStatus, &prevStatus)
t.logger.Infof("StatefulSet %s/%s is ready", tr.Namespace, tr.ResourceName)
return nil
case status := <-tr.Failed:
displayStatefulSetStatusProgress(out, formatResourceCaption(resourceName, false, true), status, &prevStatus)
return fmt.Errorf("statefulset %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason)
case status := <-tr.Status:
if status.StatusGeneration > prevStatus.StatusGeneration {
displayStatefulSetStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
prevStatus = status
}
case msg := <-tr.EventMsg:
t.logger.Infof("sts/%s: %s", tr.ResourceName, msg)
case chunk := <-tr.PodLogChunk:
t.logPodLogChunk(chunk.PodName, chunk.LogLines)
case report := <-tr.PodError:
t.logger.Warnf("sts/%s pod %s: %s: %s", tr.ResourceName, report.ReplicaSetPodError.PodName, report.ReplicaSetPodError.ContainerName, report.ReplicaSetPodError.Message)
case <-tr.AddedPod:
case err := <-trackErrCh:
return err
case <-doneCh:
@@ -364,13 +409,34 @@ func (t *Tracker) runDaemonSetTracker(ctx context.Context, tr *daemonset.Tracker
}
func (t *Tracker) waitDaemonSetTracker(ctx context.Context, tr *daemonset.Tracker, trackErrCh <-chan error, doneCh <-chan struct{}) error {
var prevStatus daemonset.DaemonSetStatus
out := statusOutput()
resourceName := fmt.Sprintf("ds/%s", tr.ResourceName)
for {
select {
case status := <-tr.Added:
displayDaemonSetStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
prevStatus = status
case <-tr.Ready:
t.logger.Debugf("DaemonSet %s/%s is ready", tr.Namespace, tr.ResourceName)
displayDaemonSetStatusProgress(out, formatResourceCaption(resourceName, true, false), prevStatus, &prevStatus)
t.logger.Infof("DaemonSet %s/%s is ready", tr.Namespace, tr.ResourceName)
return nil
case status := <-tr.Failed:
displayDaemonSetStatusProgress(out, formatResourceCaption(resourceName, false, true), status, &prevStatus)
return fmt.Errorf("daemonset %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason)
case status := <-tr.Status:
if status.StatusGeneration > prevStatus.StatusGeneration {
displayDaemonSetStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
prevStatus = status
}
case msg := <-tr.EventMsg:
t.logger.Infof("ds/%s: %s", tr.ResourceName, msg)
case chunk := <-tr.PodLogChunk:
t.logPodLogChunk(chunk.PodName, chunk.LogLines)
case report := <-tr.PodError:
t.logger.Warnf("ds/%s pod %s: %s: %s", tr.ResourceName, report.PodError.PodName, report.PodError.ContainerName, report.PodError.Message)
case <-tr.AddedPod:
case err := <-trackErrCh:
return err
case <-doneCh:
@@ -390,13 +456,34 @@ func (t *Tracker) runJobTracker(ctx context.Context, tr *job.Tracker, errCh chan
}
func (t *Tracker) waitJobTracker(ctx context.Context, tr *job.Tracker, trackErrCh <-chan error, doneCh <-chan struct{}) error {
var prevStatus job.JobStatus
out := statusOutput()
resourceName := fmt.Sprintf("job/%s", tr.ResourceName)
for {
select {
case status := <-tr.Added:
displayJobStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
prevStatus = status
case <-tr.Succeeded:
t.logger.Debugf("Job %s/%s succeeded", tr.Namespace, tr.ResourceName)
displayJobStatusProgress(out, formatResourceCaption(resourceName, true, false), prevStatus, &prevStatus)
t.logger.Infof("Job %s/%s succeeded", tr.Namespace, tr.ResourceName)
return nil
case status := <-tr.Failed:
displayJobStatusProgress(out, formatResourceCaption(resourceName, false, true), status, &prevStatus)
return fmt.Errorf("job %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason)
case status := <-tr.Status:
if status.StatusGeneration > prevStatus.StatusGeneration {
displayJobStatusProgress(out, formatResourceCaption(resourceName, false, false), status, &prevStatus)
prevStatus = status
}
case msg := <-tr.EventMsg:
t.logger.Infof("job/%s: %s", tr.ResourceName, msg)
case chunk := <-tr.PodLogChunk:
t.logPodLogChunk(chunk.PodName, chunk.LogLines)
case report := <-tr.PodError:
t.logger.Warnf("job/%s pod %s: %s: %s", tr.ResourceName, report.PodError.PodName, report.PodError.ContainerName, report.PodError.Message)
case <-tr.AddedPod:
case err := <-trackErrCh:
return err
case <-doneCh:
@@ -416,13 +503,44 @@ func (t *Tracker) runCanaryTracker(ctx context.Context, tr *canary.Tracker, errC
}
func (t *Tracker) waitCanaryTracker(ctx context.Context, tr *canary.Tracker, trackErrCh <-chan error, doneCh <-chan struct{}) error {
out := statusOutput()
resourceName := fmt.Sprintf("canary/%s", tr.ResourceName)
var lastView CanaryStatusView
for {
select {
case status := <-tr.Added:
view := CanaryStatusView{
Phase: string(status.CanaryStatus.Phase),
IsFailed: status.IsFailed,
}
displayCanaryStatus(out, formatResourceCaption(resourceName, false, false), view)
lastView = view
case <-tr.Succeeded:
t.logger.Debugf("Canary %s/%s succeeded", tr.Namespace, tr.ResourceName)
displayCanaryStatus(out, formatResourceCaption(resourceName, true, false), CanaryStatusView{Phase: lastView.Phase})
t.logger.Infof("Canary %s/%s succeeded", tr.Namespace, tr.ResourceName)
return nil
case status := <-tr.Failed:
displayCanaryStatus(out, formatResourceCaption(resourceName, false, true), CanaryStatusView{
Phase: status.FailedReason,
IsFailed: true,
})
return fmt.Errorf("canary %s/%s failed: %s", tr.Namespace, tr.ResourceName, status.FailedReason)
case status := <-tr.Status:
view := CanaryStatusView{
Phase: func() string {
if status.StatusIndicator != nil {
return status.StatusIndicator.Value
}
return ""
}(),
Age: status.Age,
IsFailed: status.IsFailed,
}
displayCanaryStatus(out, formatResourceCaption(resourceName, false, false), view)
lastView = view
case msg := <-tr.EventMsg:
t.logger.Infof("canary/%s: %s", tr.ResourceName, msg)
case err := <-trackErrCh:
return err
case <-doneCh:
@@ -433,6 +551,12 @@ func (t *Tracker) waitCanaryTracker(ctx context.Context, tr *canary.Tracker, tra
}
}
func (t *Tracker) logPodLogChunk(podName string, logLines []display.LogLine) {
for _, line := range logLines {
t.logger.Infof("po/%s [%s] %s", podName, line.Timestamp, line.Message)
}
}
func (t *Tracker) buildTargets(resources []*resource.Resource) []trackTarget {
var targets []trackTarget
for _, res := range resources {