k8sgpt/pkg/analyzer/pod_test.go
gossion dceda9a6a1
fix: pod analyzer catches errors when containers are in Terminated state (#1438)
Signed-off-by: Guoxun Wei <guwe@microsoft.com>
Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
2025-04-08 10:49:53 +01:00

423 lines
11 KiB
Go

/*
Copyright 2023 The K8sGPT Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package analyzer
import (
"context"
"sort"
"testing"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
func TestPodAnalyzer(t *testing.T) {
tests := []struct {
name string
config common.Analyzer
expectations []struct {
name string
failuresCount int
}
}{
{
name: "Pending pods, namespace filtering and readiness probe failure",
config: common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "Pod1",
Namespace: "default",
},
Status: v1.PodStatus{
Phase: v1.PodPending,
Conditions: []v1.PodCondition{
{
// This condition will contribute to failures.
Type: v1.PodScheduled,
Reason: "Unschedulable",
Message: "0/1 nodes are available: 1 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn't tolerate.",
},
{
// This condition won't contribute to failures.
Type: v1.PodScheduled,
Reason: "Unexpected failure",
},
},
},
},
&v1.Pod{
// This pod won't be selected because of namespace filtering.
ObjectMeta: metav1.ObjectMeta{
Name: "Pod2",
Namespace: "test",
},
},
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "Pod3",
Namespace: "default",
},
Status: v1.PodStatus{
// When pod is Running but its ReadinessProbe fails
Phase: v1.PodRunning,
ContainerStatuses: []v1.ContainerStatus{
{
Ready: false,
},
},
},
},
&v1.Event{
ObjectMeta: metav1.ObjectMeta{
Name: "Event1",
Namespace: "default",
},
InvolvedObject: v1.ObjectReference{
Kind: "Pod",
Name: "Pod3",
Namespace: "default",
},
Reason: "Unhealthy",
Message: "readiness probe failed: the detail reason here ...",
Source: v1.EventSource{Component: "eventTest"},
Count: 1,
Type: v1.EventTypeWarning,
},
),
},
Context: context.Background(),
Namespace: "default",
},
expectations: []struct {
name string
failuresCount int
}{
{
name: "default/Pod1",
failuresCount: 1,
},
{
name: "default/Pod3",
failuresCount: 1,
},
},
},
{
name: "readiness probe failure without any event",
config: common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "Pod1",
Namespace: "default",
},
Status: v1.PodStatus{
// When pod is Running but its ReadinessProbe fails
// It won't contribute to any failures because
// there's no event present.
Phase: v1.PodRunning,
ContainerStatuses: []v1.ContainerStatus{
{
Ready: false,
},
},
},
},
),
},
Context: context.Background(),
Namespace: "default",
},
},
{
name: "Init container status state waiting",
config: common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "Pod1",
Namespace: "default",
},
Status: v1.PodStatus{
Phase: v1.PodPending,
InitContainerStatuses: []v1.ContainerStatus{
{
Ready: true,
State: v1.ContainerState{
Running: &v1.ContainerStateRunning{
StartedAt: metav1.Now(),
},
},
},
{
Ready: false,
State: v1.ContainerState{
Waiting: &v1.ContainerStateWaiting{
// This represents a container that is still being created or blocked due to conditions such as OOMKilled
Reason: "ContainerCreating",
},
},
},
},
},
},
&v1.Event{
ObjectMeta: metav1.ObjectMeta{
Name: "Event1",
Namespace: "default",
},
InvolvedObject: v1.ObjectReference{
Kind: "Pod",
Name: "Pod1",
Namespace: "default",
},
Reason: "FailedCreatePodSandBox",
Message: "failed to create the pod sandbox ...",
Type: v1.EventTypeWarning,
},
),
},
Context: context.Background(),
Namespace: "default",
},
expectations: []struct {
name string
failuresCount int
}{
{
name: "default/Pod1",
failuresCount: 1,
},
},
},
{
name: "Container status state waiting but no event reported",
config: common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "Pod1",
Namespace: "default",
},
Status: v1.PodStatus{
Phase: v1.PodPending,
ContainerStatuses: []v1.ContainerStatus{
{
Ready: false,
State: v1.ContainerState{
Waiting: &v1.ContainerStateWaiting{
// This represents a container that is still being created or blocked due to conditions such as OOMKilled
Reason: "ContainerCreating",
},
},
},
},
},
},
),
},
Context: context.Background(),
Namespace: "default",
},
},
{
name: "Container status state waiting",
config: common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "Pod1",
Namespace: "default",
},
Status: v1.PodStatus{
Phase: v1.PodPending,
ContainerStatuses: []v1.ContainerStatus{
{
Name: "Container1",
Ready: false,
State: v1.ContainerState{
Waiting: &v1.ContainerStateWaiting{
// This represents a container that is still being created or blocked due to conditions such as OOMKilled
Reason: "ContainerCreating",
},
},
},
{
Name: "Container2",
Ready: false,
State: v1.ContainerState{
Waiting: &v1.ContainerStateWaiting{
// This represents container that is in CrashLoopBackOff state due to conditions such as OOMKilled
Reason: "CrashLoopBackOff",
},
},
LastTerminationState: v1.ContainerState{
Terminated: &v1.ContainerStateTerminated{
Reason: "test reason",
},
},
},
{
Name: "Container3",
Ready: false,
State: v1.ContainerState{
Waiting: &v1.ContainerStateWaiting{
// This won't contribute to failures.
Reason: "RandomReason",
Message: "This container won't be present in the failures",
},
},
},
{
Name: "Container4",
Ready: false,
State: v1.ContainerState{
Waiting: &v1.ContainerStateWaiting{
// Valid error reason.
Reason: "PreStartHookError",
Message: "Container4 encountered PreStartHookError",
},
},
},
{
Name: "Container5",
Ready: false,
State: v1.ContainerState{
Waiting: &v1.ContainerStateWaiting{
// Valid error reason.
Reason: "CrashLoopBackOff",
Message: "Container4 encountered CrashLoopBackOff",
},
},
},
},
},
},
&v1.Event{
ObjectMeta: metav1.ObjectMeta{
Name: "Event1",
Namespace: "default",
},
InvolvedObject: v1.ObjectReference{
Kind: "Pod",
Name: "Pod1",
Namespace: "default",
},
// This reason won't contribute to failures.
Reason: "RandomEvent",
Type: v1.EventTypeWarning,
},
),
},
Context: context.Background(),
Namespace: "default",
},
expectations: []struct {
name string
failuresCount int
}{
{
name: "default/Pod1",
failuresCount: 3,
},
},
},
{
name: "Terminated container with non-zero exit code",
config: common.Analyzer{
Client: &kubernetes.Client{
Client: fake.NewSimpleClientset(
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "Pod1",
Namespace: "default",
},
Status: v1.PodStatus{
Phase: v1.PodFailed,
ContainerStatuses: []v1.ContainerStatus{
{
Name: "Container1",
Ready: false,
State: v1.ContainerState{
Terminated: &v1.ContainerStateTerminated{
ExitCode: 1,
Reason: "Error",
},
},
},
{
Name: "Container2",
Ready: false,
State: v1.ContainerState{
Terminated: &v1.ContainerStateTerminated{
ExitCode: 2,
Reason: "",
},
},
},
},
},
},
),
},
Context: context.Background(),
Namespace: "default",
},
expectations: []struct {
name string
failuresCount int
}{
{
name: "default/Pod1",
failuresCount: 2,
},
},
},
}
podAnalyzer := PodAnalyzer{}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
results, err := podAnalyzer.Analyze(tt.config)
require.NoError(t, err)
if tt.expectations == nil {
require.Equal(t, 0, len(results))
} else {
sort.Slice(results, func(i, j int) bool {
return results[i].Name < results[j].Name
})
require.Equal(t, len(tt.expectations), len(results))
for i, result := range results {
require.Equal(t, tt.expectations[i].name, result.Name)
require.Equal(t, tt.expectations[i].failuresCount, len(result.Error))
}
}
})
}
}