Skip to content

Commit 19004b9

Browse files
committed
fix(k8s): surface container status in PodNotAvailable error
When WaitUntilPodAvailable times out, the error only reported the pod-level reason and message, which are usually empty when a container fails to start. The actionable detail (CrashLoopBackOff, ImagePullBackOff, etc.) lives in the container statuses, so include the waiting/terminated state of each unready container, covering init and regular containers. Closes #1251
1 parent 0e1defc commit 19004b9

2 files changed

Lines changed: 100 additions & 1 deletion

File tree

modules/k8s/errors.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package k8s
33
import (
44
"errors"
55
"fmt"
6+
"strings"
67

78
appsv1 "k8s.io/api/apps/v1"
89
batchv1 "k8s.io/api/batch/v1"
@@ -145,7 +146,43 @@ type PodNotAvailable struct {
145146

146147
// Error is a simple function to return a formatted error message as a string
147148
func (err PodNotAvailable) Error() string {
148-
return fmt.Sprintf("Pod %s is not available, reason: %s, message: %s", err.pod.Name, err.pod.Status.Reason, err.pod.Status.Message)
149+
msg := fmt.Sprintf("Pod %s is not available, reason: %s, message: %s", err.pod.Name, err.pod.Status.Reason, err.pod.Status.Message)
150+
151+
// The pod-level reason and message above are usually empty when a container fails to start. The actionable detail
152+
// (for example CrashLoopBackOff or ImagePullBackOff) lives in the container statuses, so surface it here.
153+
details := append(
154+
unreadyContainerDetails("init container", err.pod.Status.InitContainerStatuses),
155+
unreadyContainerDetails("container", err.pod.Status.ContainerStatuses)...,
156+
)
157+
if len(details) > 0 {
158+
msg += ". " + strings.Join(details, "; ")
159+
}
160+
161+
return msg
162+
}
163+
164+
// unreadyContainerDetails returns a short description of the waiting or terminated state of each container that is not
165+
// ready, prefixed with the given kind (for example "container" or "init container").
166+
func unreadyContainerDetails(kind string, statuses []corev1.ContainerStatus) []string {
167+
var details []string
168+
169+
for i := range statuses {
170+
status := statuses[i]
171+
if status.Ready {
172+
continue
173+
}
174+
175+
switch {
176+
case status.State.Waiting != nil:
177+
detail := fmt.Sprintf("%s %s waiting: %s", kind, status.Name, status.State.Waiting.Reason)
178+
details = append(details, strings.TrimSpace(detail+" "+status.State.Waiting.Message))
179+
case status.State.Terminated != nil:
180+
detail := fmt.Sprintf("%s %s terminated: %s", kind, status.Name, status.State.Terminated.Reason)
181+
details = append(details, strings.TrimSpace(detail+" "+status.State.Terminated.Message))
182+
}
183+
}
184+
185+
return details
149186
}
150187

151188
// NewPodNotAvailableError returns a PodNotAvailable struct when Kubernetes deems a pod is not available

modules/k8s/errors_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,65 @@ func TestErrorDeploymentNotAvailable(t *testing.T) {
6262
})
6363
}
6464
}
65+
66+
func TestErrorPodNotAvailable(t *testing.T) {
67+
t.Parallel()
68+
69+
testCases := []struct {
70+
pod *v1.Pod
71+
title string
72+
expectedErr string
73+
}{
74+
{
75+
title: "PodLevelReasonOnly",
76+
pod: &v1.Pod{
77+
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
78+
Status: v1.PodStatus{Reason: "Evicted", Message: "low memory"},
79+
},
80+
expectedErr: "Pod foo is not available, reason: Evicted, message: low memory",
81+
},
82+
{
83+
title: "ContainerWaiting",
84+
pod: &v1.Pod{
85+
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
86+
Status: v1.PodStatus{
87+
ContainerStatuses: []v1.ContainerStatus{
88+
{
89+
Name: "web",
90+
Ready: false,
91+
State: v1.ContainerState{Waiting: &v1.ContainerStateWaiting{
92+
Reason: "CrashLoopBackOff",
93+
Message: "back-off 5m restarting failed container",
94+
}},
95+
},
96+
},
97+
},
98+
},
99+
expectedErr: "Pod foo is not available, reason: , message: . container web waiting: CrashLoopBackOff back-off 5m restarting failed container",
100+
},
101+
{
102+
title: "InitContainerWaitingAndReadyContainerIgnored",
103+
pod: &v1.Pod{
104+
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
105+
Status: v1.PodStatus{
106+
InitContainerStatuses: []v1.ContainerStatus{
107+
{Name: "setup", Ready: false, State: v1.ContainerState{Waiting: &v1.ContainerStateWaiting{Reason: "ImagePullBackOff"}}},
108+
},
109+
ContainerStatuses: []v1.ContainerStatus{
110+
{Name: "web", Ready: true},
111+
},
112+
},
113+
},
114+
expectedErr: "Pod foo is not available, reason: , message: . init container setup waiting: ImagePullBackOff",
115+
},
116+
}
117+
118+
for _, tc := range testCases {
119+
t.Run(tc.title, func(t *testing.T) {
120+
t.Parallel()
121+
122+
err := k8s.NewPodNotAvailableError(tc.pod)
123+
assert.EqualError(t, err, tc.expectedErr)
124+
})
125+
}
126+
}

0 commit comments

Comments
 (0)