mirror of
https://github.com/k8sgpt-ai/k8sgpt.git
synced 2025-07-31 07:07:33 +00:00
Signed-off-by: Guoxun Wei <guwe@microsoft.com> Co-authored-by: Alex Jones <alexsimonjones@gmail.com>
423 lines
11 KiB
Go
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))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|