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 errormine
parent
2daec83d60
commit
41acad343b
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue