fix: consider readinessGates + ready condition in diagnose (#3556)

* Consider readinessGates + ready condition in diagnose

Now that readiness gates are supported, those should be considered when displaying a pod healthiness

Additionally consider the pod ready condition status. It should match the && of the containers' ready condition and readiness gates but I've actually observed that in some cases, it can be false with the containers reporting ready true.

* Update pod.go

* Update pod.go

fix lint

* Update pod.go

lint

* add tests for diagnose and readinessGateStats

* lint error
mine
jfremy-openai 2025-10-05 07:38:03 -07:00 committed by GitHub
parent 2daec83d60
commit 41acad343b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 172 additions and 3 deletions

View File

@ -165,6 +165,8 @@ func (p *Pod) defaultRow(pwm *PodWithMetrics, row *model1.Row) error {
iReady, iTerminated, iRestarts := p.initContainerStats(spec.InitContainers, st.InitContainerStatuses) iReady, iTerminated, iRestarts := p.initContainerStats(spec.InitContainers, st.InitContainerStatuses)
cReady += iReady cReady += iReady
allCounts := len(spec.Containers) + iTerminated allCounts := len(spec.Containers) + iTerminated
rgr, rgt := p.readinessGateStats(spec, &st)
ready := hasPodReadyCondition(st.Conditions)
var ccmx []mv1beta1.ContainerMetrics var ccmx []mv1beta1.ContainerMetrics
if pwm.MX != nil { if pwm.MX != nil {
@ -201,7 +203,7 @@ func (p *Pod) defaultRow(pwm *PodWithMetrics, row *model1.Row) error {
asReadinessGate(spec, &st), asReadinessGate(spec, &st),
p.mapQOS(st.QOSClass), p.mapQOS(st.QOSClass),
mapToStr(pwm.Raw.GetLabels()), mapToStr(pwm.Raw.GetLabels()),
AsStatus(p.diagnose(phase, cReady, allCounts)), AsStatus(p.diagnose(phase, cReady, allCounts, ready, rgr, rgt)),
ToAge(pwm.Raw.GetCreationTimestamp()), ToAge(pwm.Raw.GetCreationTimestamp()),
} }
@ -234,16 +236,25 @@ func (p *Pod) Healthy(_ context.Context, o any) error {
cr += icr cr += icr
ct += ict ct += ict
return p.diagnose(phase, cr, ct) ready := hasPodReadyCondition(st.Conditions)
rgr, rgt := p.readinessGateStats(spec, &st)
return p.diagnose(phase, cr, ct, ready, rgr, rgt)
} }
func (*Pod) diagnose(phase string, cr, ct int) error { func (*Pod) diagnose(phase string, cr, ct int, ready bool, rgr, rgt int) error {
if phase == Completed { if phase == Completed {
return nil return nil
} }
if cr != ct || ct == 0 { if cr != ct || ct == 0 {
return fmt.Errorf("container ready check failed: %d of %d", cr, ct) return fmt.Errorf("container ready check failed: %d of %d", cr, ct)
} }
if rgt > 0 && rgr != rgt {
return fmt.Errorf("readiness gate check failed: %d of %d", rgr, rgt)
}
if !ready {
return fmt.Errorf("pod condition ready is false")
}
if phase == Terminating { if phase == Terminating {
return fmt.Errorf("pod is terminating") return fmt.Errorf("pod is terminating")
} }
@ -428,6 +439,20 @@ func (*Pod) initContainerStats(cc []v1.Container, cos []v1.ContainerStatus) (rea
return return
} }
func (*Pod) readinessGateStats(spec *v1.PodSpec, st *v1.PodStatus) (ready, total int) {
total = len(spec.ReadinessGates)
for _, readinessGate := range spec.ReadinessGates {
for _, condition := range st.Conditions {
if condition.Type == readinessGate.ConditionType {
if condition.Status == "True" {
ready++
}
}
}
}
return
}
// Phase reports the given pod phase. // Phase reports the given pod phase.
func (p *Pod) Phase(dt *metav1.Time, spec *v1.PodSpec, st *v1.PodStatus) string { func (p *Pod) Phase(dt *metav1.Time, spec *v1.PodSpec, st *v1.PodStatus) string {
status := string(st.Phase) status := string(st.Phase)

View File

@ -10,6 +10,7 @@ import (
"github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/client"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
res "k8s.io/apimachinery/pkg/api/resource" res "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -629,6 +630,149 @@ func Test_podRequests(t *testing.T) {
} }
} }
func Test_readinessGateStats(t *testing.T) {
const (
gate1 = "k9s.derailed.com/gate1"
gate2 = "k9s.derailed.com/gate2"
)
uu := map[string]struct {
spec *v1.PodSpec
st *v1.PodStatus
r int
t int
}{
"empty": {
spec: &v1.PodSpec{},
st: &v1.PodStatus{
Conditions: []v1.PodCondition{{Type: v1.PodReady, Status: v1.ConditionTrue}},
},
r: 0,
t: 0,
},
"single": {
spec: &v1.PodSpec{
ReadinessGates: []v1.PodReadinessGate{{ConditionType: gate1}},
},
st: &v1.PodStatus{
Conditions: []v1.PodCondition{{Type: gate1, Status: v1.ConditionTrue}},
},
r: 1,
t: 1,
},
"multiple": {
spec: &v1.PodSpec{
ReadinessGates: []v1.PodReadinessGate{{ConditionType: gate1}, {ConditionType: gate2}},
},
st: &v1.PodStatus{
Conditions: []v1.PodCondition{{Type: gate1, Status: v1.ConditionTrue}, {Type: gate2, Status: v1.ConditionTrue}, {Type: v1.PodReady, Status: v1.ConditionFalse}},
},
r: 2,
t: 2,
},
"mixed": {
spec: &v1.PodSpec{
ReadinessGates: []v1.PodReadinessGate{{ConditionType: gate1}, {ConditionType: gate2}},
},
st: &v1.PodStatus{
Conditions: []v1.PodCondition{{Type: gate1, Status: v1.ConditionTrue}, {Type: gate2, Status: v1.ConditionFalse}, {Type: v1.PodReady, Status: v1.ConditionTrue}},
},
r: 1,
t: 2,
},
"missing": {
spec: &v1.PodSpec{
ReadinessGates: []v1.PodReadinessGate{{ConditionType: gate1}, {ConditionType: gate2}},
},
st: &v1.PodStatus{
Conditions: []v1.PodCondition{{Type: gate1, Status: v1.ConditionTrue}, {Type: v1.PodReady, Status: v1.ConditionTrue}},
},
r: 1,
t: 2,
},
}
var p Pod
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
ready, total := p.readinessGateStats(u.spec, u.st)
assert.Equal(t, u.r, ready)
assert.Equal(t, u.t, total)
})
}
}
func Test_diagnose(t *testing.T) {
uu := map[string]struct {
phase string
cr, ct int
ready bool
rgr, rgt int
err string
}{
"completed": {
phase: Completed,
cr: 0,
ct: 1,
ready: true,
rgr: 0,
rgt: 0,
err: "",
},
"container-ready-check-failed": {
phase: "Running",
cr: 1,
ct: 2,
ready: true,
rgr: 1,
rgt: 2,
err: "container ready check failed: 1 of 2",
},
"readiness-gate-check-failed": {
phase: "Running",
cr: 1,
ct: 1,
ready: true,
rgr: 1,
rgt: 2,
err: "readiness gate check failed: 1 of 2",
},
"pod-condition-ready-false": {
phase: "Running",
cr: 1,
ct: 1,
ready: false,
rgr: 0,
rgt: 0,
err: "pod condition ready is false",
},
"pod-terminating": {
phase: "Terminating",
cr: 1,
ct: 1,
ready: true,
rgr: 1,
rgt: 1,
err: "pod is terminating",
},
}
var p Pod
for k := range uu {
u := uu[k]
t.Run(k, func(t *testing.T) {
err := p.diagnose(u.phase, u.cr, u.ct, u.ready, u.rgr, u.rgt)
if u.err == "" {
assert.NoError(t, err)
} else {
require.Error(t, err)
assert.Contains(t, err.Error(), u.err)
}
})
}
}
// Helpers... // Helpers...
func makeContainer(n string, restartable bool, rc, rm, lc, lm string) v1.Container { func makeContainer(n string, restartable bool, rc, rm, lc, lm string) v1.Container {