Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
.idea/
_tmp/
.vscode
.work
87 changes: 72 additions & 15 deletions pkg/cli/debug/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"os"
"reflect"
"slices"
"strings"
"time"

Expand Down Expand Up @@ -43,6 +44,7 @@ import (
"k8s.io/kubectl/pkg/util/interrupt"
"k8s.io/kubectl/pkg/util/templates"
"k8s.io/pod-security-admission/api"
utilexec "k8s.io/utils/exec"

appsv1 "github.com/openshift/api/apps/v1"
dockerv10 "github.com/openshift/api/image/docker10"
Expand Down Expand Up @@ -614,7 +616,7 @@ func (o *DebugOptions) RunDebug() error {
}
return errors.New(msg)
// switch to logging output
case err == krun.ErrPodCompleted, err == conditions.ErrContainerTerminated:
case errors.Is(err, krun.ErrPodCompleted), errors.Is(err, conditions.ErrContainerTerminated):
resultPod, ok := containerRunningEvent.Object.(*corev1.Pod)
if ok {
if resultPod.Status.Reason == "NodeAffinity" && len(resultPod.Spec.NodeSelector) != 0 {
Expand All @@ -630,12 +632,34 @@ func (o *DebugOptions) RunDebug() error {
}
}
}
return o.getLogs(pod)
case err == conditions.ErrNonZeroExitCode:

if err := o.getLogs(pod); err != nil {
return err
}

// The watch event contains the terminal pod state from the API server.
// Use it directly to extract the exit code.
if ok {
return exitCodeError(resultPod, o.ContainerName)
}
return nil
case errors.Is(err, conditions.ErrNonZeroExitCode):
if err = o.getLogs(pod); err != nil {
return err
}
return conditions.ErrNonZeroExitCode

// The watch event contains the terminal pod state from the API server.
// Use it directly to extract the exit code.
if resultPod, ok := containerRunningEvent.Object.(*corev1.Pod); ok {
if exitErr := exitCodeError(resultPod, o.ContainerName); exitErr != nil {
return exitErr
}
}
// We know the exit code was non-zero but couldn't determine the actual value.
return utilexec.CodeExitError{
Err: fmt.Errorf("the debug container terminated with a non-zero exit code"),
Code: 1,
}
case err != nil:
return err
case !o.Attach.Stdin:
Expand All @@ -645,20 +669,14 @@ func (o *DebugOptions) RunDebug() error {
lastWatchEvent, err := watchtools.UntilWithSync(ctx, lw, &corev1.Pod{}, preconditionFunc, conditions.PodDone)
if err != nil {
if kapierrors.IsNotFound(err) {
return nil
return fmt.Errorf("the debug pod %q was deleted before completion", pod.Name)
}
return err
}

resultPod, ok := lastWatchEvent.Object.(*corev1.Pod)
if ok {
for _, s := range append(append([]corev1.ContainerStatus{}, resultPod.Status.InitContainerStatuses...), resultPod.Status.ContainerStatuses...) {
if s.Name != o.ContainerName {
continue
}
if s.State.Terminated != nil && s.State.Terminated.ExitCode != 0 {
return conditions.ErrNonZeroExitCode
}
if resultPod, ok := lastWatchEvent.Object.(*corev1.Pod); ok {
if exitErr := exitCodeError(resultPod, o.ContainerName); exitErr != nil {
return exitErr
}
}
return nil
Expand All @@ -673,7 +691,17 @@ func (o *DebugOptions) RunDebug() error {

// TODO: attach can race with pod completion, allow attach to switch to logs
o.Attach.ContainerName = o.ContainerName
return o.Attach.Run()
if err := o.Attach.Run(); err != nil {
return err
}

// After the attach session ends, check the container exit code.
resultPod, err := o.CoreClient.Pods(pod.Namespace).Get(context.TODO(), pod.Name, metav1.GetOptions{})
if err != nil {
klog.V(4).Infof("Unable to re-fetch pod %s/%s after attach: %v", pod.Namespace, pod.Name, err)
return nil
}
return exitCodeError(resultPod, o.ContainerName)
}
})
}
Expand Down Expand Up @@ -1238,6 +1266,35 @@ func (o *DebugOptions) approximatePodTemplateForObject(object runtime.Object) (*
return nil, fmt.Errorf("%v is not supported by debug", reflect.TypeOf(object))
}

// containerExitCode returns the exit code of the named container from the pod status.
// It returns -1 if the container is not found or has not terminated.
func containerExitCode(pod *corev1.Pod, containerName string) int32 {
for _, s := range slices.Concat(pod.Status.InitContainerStatuses, pod.Status.ContainerStatuses) {
if s.Name != containerName {
continue
}
if s.State.Terminated != nil {
return s.State.Terminated.ExitCode
}
return -1
}
return -1
}

// exitCodeError returns a CodeExitError with the container's actual exit code if it
// terminated with a non-zero exit code. It returns nil if the container exited
// successfully or if exit code information is not available.
func exitCodeError(pod *corev1.Pod, containerName string) error {
code := containerExitCode(pod, containerName)
if code <= 0 {
return nil
}
return utilexec.CodeExitError{
Err: fmt.Errorf("the debug container terminated with exit code %d", code),
Code: int(code),
}
}

func (o *DebugOptions) getLogs(pod *corev1.Pod) error {
return logs.LogsOptions{
Object: pod,
Expand Down
208 changes: 208 additions & 0 deletions pkg/cli/debug/debug_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package debug

import (
"errors"
"testing"

corev1 "k8s.io/api/core/v1"
Expand All @@ -10,6 +11,7 @@ import (
"k8s.io/kubectl/pkg/cmd/attach"
"k8s.io/kubectl/pkg/cmd/exec"
"k8s.io/pod-security-admission/api"
utilexec "k8s.io/utils/exec"

fakekubeclient "k8s.io/client-go/kubernetes/fake"
fakecorev1client "k8s.io/client-go/kubernetes/typed/core/v1/fake"
Expand Down Expand Up @@ -266,3 +268,209 @@ func TestCreateLabelMap(t *testing.T) {
})
}
}

func TestContainerExitCode(t *testing.T) {
tests := []struct {
name string
pod *corev1.Pod
containerName string
expectedExitCode int32
}{
{
name: "terminated with non-zero exit code",
pod: &corev1.Pod{
Status: corev1.PodStatus{
ContainerStatuses: []corev1.ContainerStatus{
{
Name: "debug",
State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{ExitCode: 42}},
},
},
},
},
containerName: "debug",
expectedExitCode: 42,
},
{
name: "terminated with exit code 0",
pod: &corev1.Pod{
Status: corev1.PodStatus{
ContainerStatuses: []corev1.ContainerStatus{
{
Name: "debug",
State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{ExitCode: 0}},
},
},
},
},
containerName: "debug",
expectedExitCode: 0,
},
{
name: "container still running",
pod: &corev1.Pod{
Status: corev1.PodStatus{
ContainerStatuses: []corev1.ContainerStatus{
{
Name: "debug",
State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{}},
},
},
},
},
containerName: "debug",
expectedExitCode: -1,
},
{
name: "container not found",
pod: &corev1.Pod{
Status: corev1.PodStatus{
ContainerStatuses: []corev1.ContainerStatus{
{
Name: "other",
State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{ExitCode: 1}},
},
},
},
},
containerName: "debug",
expectedExitCode: -1,
},
{
name: "init container terminated with non-zero exit code",
pod: &corev1.Pod{
Status: corev1.PodStatus{
InitContainerStatuses: []corev1.ContainerStatus{
{
Name: "init",
State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{ExitCode: 137}},
},
},
},
},
containerName: "init",
expectedExitCode: 137,
},
{
name: "empty pod status",
pod: &corev1.Pod{},
containerName: "debug",
expectedExitCode: -1,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := containerExitCode(tt.pod, tt.containerName)
if got != tt.expectedExitCode {
t.Errorf("containerExitCode() = %d, want %d", got, tt.expectedExitCode)
}
})
}
}

func TestExitCodeError(t *testing.T) {
tests := []struct {
name string
pod *corev1.Pod
containerName string
// expectedExitCode 0 is used to expect exitCodeError to return nil.
expectedExitCode int
}{
{
name: "non-zero exit code returns CodeExitError",
pod: &corev1.Pod{
Status: corev1.PodStatus{
ContainerStatuses: []corev1.ContainerStatus{
{
Name: "debug",
State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{ExitCode: 42}},
},
},
},
},
containerName: "debug",
expectedExitCode: 42,
},
{
name: "exit code 0 returns nil",
pod: &corev1.Pod{
Status: corev1.PodStatus{
ContainerStatuses: []corev1.ContainerStatus{
{
Name: "debug",
State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{ExitCode: 0}},
},
},
},
},
containerName: "debug",
expectedExitCode: 0,
},
{
name: "container not terminated returns nil",
pod: &corev1.Pod{
Status: corev1.PodStatus{
ContainerStatuses: []corev1.ContainerStatus{
{
Name: "debug",
State: corev1.ContainerState{Running: &corev1.ContainerStateRunning{}},
},
},
},
},
containerName: "debug",
expectedExitCode: 0,
},
{
name: "container not found returns nil",
pod: &corev1.Pod{
Status: corev1.PodStatus{
ContainerStatuses: []corev1.ContainerStatus{
{
Name: "other",
State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{ExitCode: 1}},
},
},
},
},
containerName: "debug",
expectedExitCode: 0,
},
{
name: "exit code 127 returns correct code",
pod: &corev1.Pod{
Status: corev1.PodStatus{
ContainerStatuses: []corev1.ContainerStatus{
{
Name: "debug",
State: corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{ExitCode: 127}},
},
},
},
},
containerName: "debug",
expectedExitCode: 127,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := exitCodeError(tt.pod, tt.containerName)
if tt.expectedExitCode == 0 {
if err != nil {
t.Errorf("expected nil, got %v", err)
}
return
}

var exitErr utilexec.CodeExitError
if !errors.As(err, &exitErr) {
t.Fatalf("expected utilexec.CodeExitError, got %T: %v", err, err)
}
if exitErr.ExitStatus() != tt.expectedExitCode {
t.Errorf("ExitStatus() = %d, want %d", exitErr.ExitStatus(), tt.expectedExitCode)
}
})
}
}
Loading