mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-05 00:00:51 +02:00
* 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>
454 lines
13 KiB
Go
454 lines
13 KiB
Go
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())
|
|
}
|