Skip to content

Commit ad3d894

Browse files
kubelet: Preserve existing container status when pod terminated
The kubelet must not allow a container that was reported failed in a restartPolicy=Never pod to be reported to the apiserver as success. If a client deletes a restartPolicy=Never pod, the dispatchWork and status manager race to update the container status. When dispatchWork (specifically podIsTerminated) returns true, it means all containers are stopped, which means status in the container is accurate. However, the TerminatePod method then clears this status. This results in a pod that has been reported with status.phase=Failed getting reset to status.phase.Succeeded, which is a violation of the guarantees around terminal phase. Ensure the Kubelet never reports that a container succeeded when it hasn't run or been executed by guarding the terminate pod loop from ever reporting 0 in the absence of container status.
1 parent 6d98b0a commit ad3d894

File tree

4 files changed

+55
-4
lines changed

4 files changed

+55
-4
lines changed

pkg/kubelet/kubelet.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2001,9 +2001,10 @@ func (kl *Kubelet) syncLoopIteration(configCh <-chan kubetypes.PodUpdate, handle
20012001
}
20022002

20032003
// dispatchWork starts the asynchronous sync of the pod in a pod worker.
2004-
// If the pod is terminated, dispatchWork
2004+
// If the pod is terminated, dispatchWork will perform no action.
20052005
func (kl *Kubelet) dispatchWork(pod *v1.Pod, syncType kubetypes.SyncPodType, mirrorPod *v1.Pod, start time.Time) {
20062006
if kl.podIsTerminated(pod) {
2007+
klog.V(4).Infof("Pod %q is terminated, ignoring remaining sync work: %s", format.Pod(pod), syncType)
20072008
if pod.DeletionTimestamp != nil {
20082009
// If the pod is in a terminated state, there is no pod worker to
20092010
// handle the work item. Check if the DeletionTimestamp has been

pkg/kubelet/status/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ go_test(
5454
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
5555
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
5656
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
57+
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
5758
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
5859
"//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library",
5960
"//staging/src/k8s.io/client-go/testing:go_default_library",

pkg/kubelet/status/status_manager.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,21 +314,39 @@ func findContainerStatus(status *v1.PodStatus, containerID string) (containerSta
314314
func (m *manager) TerminatePod(pod *v1.Pod) {
315315
m.podStatusesLock.Lock()
316316
defer m.podStatusesLock.Unlock()
317+
318+
// ensure that all containers have a terminated state - because we do not know whether the container
319+
// was successful, always report an error
317320
oldStatus := &pod.Status
318321
if cachedStatus, ok := m.podStatuses[pod.UID]; ok {
319322
oldStatus = &cachedStatus.status
320323
}
321324
status := *oldStatus.DeepCopy()
322325
for i := range status.ContainerStatuses {
326+
if status.ContainerStatuses[i].State.Terminated != nil || status.ContainerStatuses[i].State.Waiting != nil {
327+
continue
328+
}
323329
status.ContainerStatuses[i].State = v1.ContainerState{
324-
Terminated: &v1.ContainerStateTerminated{},
330+
Terminated: &v1.ContainerStateTerminated{
331+
Reason: "ContainerStatusUnknown",
332+
Message: "The container could not be located when the pod was terminated",
333+
ExitCode: 137,
334+
},
325335
}
326336
}
327337
for i := range status.InitContainerStatuses {
338+
if status.InitContainerStatuses[i].State.Terminated != nil || status.InitContainerStatuses[i].State.Waiting != nil {
339+
continue
340+
}
328341
status.InitContainerStatuses[i].State = v1.ContainerState{
329-
Terminated: &v1.ContainerStateTerminated{},
342+
Terminated: &v1.ContainerStateTerminated{
343+
Reason: "ContainerStatusUnknown",
344+
Message: "The container could not be located when the pod was terminated",
345+
ExitCode: 137,
346+
},
330347
}
331348
}
349+
332350
m.updateStatusInternal(pod, status, true)
333351
}
334352

pkg/kubelet/status/status_manager_test.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@ import (
2727

2828
"github.com/stretchr/testify/assert"
2929

30-
"k8s.io/api/core/v1"
30+
v1 "k8s.io/api/core/v1"
3131
"k8s.io/apimachinery/pkg/api/errors"
3232
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3333
"k8s.io/apimachinery/pkg/runtime"
3434
"k8s.io/apimachinery/pkg/runtime/schema"
35+
"k8s.io/apimachinery/pkg/util/diff"
3536
clientset "k8s.io/client-go/kubernetes"
3637
"k8s.io/client-go/kubernetes/fake"
3738
core "k8s.io/client-go/testing"
@@ -569,6 +570,16 @@ func TestTerminatePod(t *testing.T) {
569570
t.Logf("update the pod's status to Failed. TerminatePod should preserve this status update.")
570571
firstStatus := getRandomPodStatus()
571572
firstStatus.Phase = v1.PodFailed
573+
firstStatus.InitContainerStatuses = []v1.ContainerStatus{
574+
{Name: "init-test-1"},
575+
{Name: "init-test-2", State: v1.ContainerState{Terminated: &v1.ContainerStateTerminated{Reason: "InitTest", ExitCode: 0}}},
576+
{Name: "init-test-3", State: v1.ContainerState{Terminated: &v1.ContainerStateTerminated{Reason: "InitTest", ExitCode: 3}}},
577+
}
578+
firstStatus.ContainerStatuses = []v1.ContainerStatus{
579+
{Name: "test-1"},
580+
{Name: "test-2", State: v1.ContainerState{Terminated: &v1.ContainerStateTerminated{Reason: "Test", ExitCode: 2}}},
581+
{Name: "test-3", State: v1.ContainerState{Terminated: &v1.ContainerStateTerminated{Reason: "Test", ExitCode: 0}}},
582+
}
572583
syncer.SetPodStatus(testPod, firstStatus)
573584

574585
t.Logf("set the testPod to a pod with Phase running, to simulate a stale pod")
@@ -586,6 +597,26 @@ func TestTerminatePod(t *testing.T) {
586597
assert.False(t, newStatus.InitContainerStatuses[i].State.Terminated == nil, "expected init containers to be terminated")
587598
}
588599

600+
expectUnknownState := v1.ContainerState{Terminated: &v1.ContainerStateTerminated{Reason: "ContainerStatusUnknown", Message: "The container could not be located when the pod was terminated", ExitCode: 137}}
601+
if !reflect.DeepEqual(newStatus.InitContainerStatuses[0].State, expectUnknownState) {
602+
t.Errorf("terminated container state not defaulted: %s", diff.ObjectReflectDiff(newStatus.InitContainerStatuses[0].State, expectUnknownState))
603+
}
604+
if !reflect.DeepEqual(newStatus.InitContainerStatuses[1].State, firstStatus.InitContainerStatuses[1].State) {
605+
t.Errorf("existing terminated container state not preserved: %#v", newStatus.ContainerStatuses)
606+
}
607+
if !reflect.DeepEqual(newStatus.InitContainerStatuses[2].State, firstStatus.InitContainerStatuses[2].State) {
608+
t.Errorf("existing terminated container state not preserved: %#v", newStatus.ContainerStatuses)
609+
}
610+
if !reflect.DeepEqual(newStatus.ContainerStatuses[0].State, expectUnknownState) {
611+
t.Errorf("terminated container state not defaulted: %s", diff.ObjectReflectDiff(newStatus.ContainerStatuses[0].State, expectUnknownState))
612+
}
613+
if !reflect.DeepEqual(newStatus.ContainerStatuses[1].State, firstStatus.ContainerStatuses[1].State) {
614+
t.Errorf("existing terminated container state not preserved: %#v", newStatus.ContainerStatuses)
615+
}
616+
if !reflect.DeepEqual(newStatus.ContainerStatuses[2].State, firstStatus.ContainerStatuses[2].State) {
617+
t.Errorf("existing terminated container state not preserved: %#v", newStatus.ContainerStatuses)
618+
}
619+
589620
t.Logf("we expect the previous status update to be preserved.")
590621
assert.Equal(t, newStatus.Phase, firstStatus.Phase)
591622
assert.Equal(t, newStatus.Message, firstStatus.Message)

0 commit comments

Comments
 (0)