Skip to content

Commit

Permalink
Merge pull request kubernetes#110479 from bobbypage/automated-cherry-…
Browse files Browse the repository at this point in the history
…pick-of-#110256-upstream-release-1.24

Automated cherry pick of kubernetes#110256: kubelet: Mark ready condition as false explicitly for terminal pods
  • Loading branch information
k8s-ci-robot authored Jun 10, 2022
2 parents c693b19 + 10b650a commit 050f930
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 16 deletions.
13 changes: 13 additions & 0 deletions pkg/api/v1/pod/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,13 +317,26 @@ func IsPodReadyConditionTrue(status v1.PodStatus) bool {
return condition != nil && condition.Status == v1.ConditionTrue
}

// IsContainersReadyConditionTrue returns true if a pod is ready; false otherwise.
func IsContainersReadyConditionTrue(status v1.PodStatus) bool {
condition := GetContainersReadyCondition(status)
return condition != nil && condition.Status == v1.ConditionTrue
}

// GetPodReadyCondition extracts the pod ready condition from the given status and returns that.
// Returns nil if the condition is not present.
func GetPodReadyCondition(status v1.PodStatus) *v1.PodCondition {
_, condition := GetPodCondition(&status, v1.PodReady)
return condition
}

// GetContainersReadyCondition extracts the containers ready condition from the given status and returns that.
// Returns nil if the condition is not present.
func GetContainersReadyCondition(status v1.PodStatus) *v1.PodCondition {
_, condition := GetPodCondition(&status, v1.ContainersReady)
return condition
}

// GetPodCondition extracts the provided condition from the given status and returns that.
// Returns nil and -1 if the condition is not present, and the index of the located condition.
func GetPodCondition(status *v1.PodStatus, conditionType v1.PodConditionType) (int, *v1.PodCondition) {
Expand Down
94 changes: 94 additions & 0 deletions pkg/api/v1/pod/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -904,3 +904,97 @@ func TestUpdatePodCondition(t *testing.T) {
assert.Equal(t, test.expected, resultStatus, test.desc)
}
}

func TestGetContainersReadyCondition(t *testing.T) {
time := metav1.Now()

containersReadyCondition := v1.PodCondition{
Type: v1.ContainersReady,
Status: v1.ConditionTrue,
Reason: "successfully",
Message: "sync pod successfully",
LastProbeTime: time,
LastTransitionTime: metav1.NewTime(time.Add(1000)),
}

tests := []struct {
desc string
podStatus v1.PodStatus
expectedCondition *v1.PodCondition
}{
{
desc: "containers ready condition exists",
podStatus: v1.PodStatus{
Conditions: []v1.PodCondition{containersReadyCondition},
},
expectedCondition: &containersReadyCondition,
},
{
desc: "containers ready condition does not exist",
podStatus: v1.PodStatus{
Conditions: []v1.PodCondition{},
},
expectedCondition: nil,
},
}

for _, test := range tests {
containersReadyCondition := GetContainersReadyCondition(test.podStatus)
assert.Equal(t, test.expectedCondition, containersReadyCondition, test.desc)
}
}

func TestIsContainersReadyConditionTrue(t *testing.T) {
time := metav1.Now()

tests := []struct {
desc string
podStatus v1.PodStatus
expected bool
}{
{
desc: "containers ready condition is true",
podStatus: v1.PodStatus{
Conditions: []v1.PodCondition{
{
Type: v1.ContainersReady,
Status: v1.ConditionTrue,
Reason: "successfully",
Message: "sync pod successfully",
LastProbeTime: time,
LastTransitionTime: metav1.NewTime(time.Add(1000)),
},
},
},
expected: true,
},
{
desc: "containers ready condition is false",
podStatus: v1.PodStatus{
Conditions: []v1.PodCondition{
{
Type: v1.ContainersReady,
Status: v1.ConditionFalse,
Reason: "successfully",
Message: "sync pod successfully",
LastProbeTime: time,
LastTransitionTime: metav1.NewTime(time.Add(1000)),
},
},
},
expected: false,
},
{
desc: "containers ready condition is empty",
podStatus: v1.PodStatus{
Conditions: []v1.PodCondition{},
},
expected: false,
},
}

for _, test := range tests {
isContainersReady := IsContainersReadyConditionTrue(test.podStatus)
assert.Equal(t, test.expected, isContainersReady, test.desc)
}
}
45 changes: 39 additions & 6 deletions pkg/kubelet/status/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
"fmt"
"strings"

"k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
podutil "k8s.io/kubernetes/pkg/api/v1/pod"
)

Expand All @@ -29,6 +29,8 @@ const (
UnknownContainerStatuses = "UnknownContainerStatuses"
// PodCompleted says that all related containers have succeeded.
PodCompleted = "PodCompleted"
// PodFailed says that the pod has failed and as such the containers have failed.
PodFailed = "PodFailed"
// ContainersNotReady says that one or more containers are not ready.
ContainersNotReady = "ContainersNotReady"
// ContainersNotInitialized says that one or more init containers have not succeeded.
Expand Down Expand Up @@ -62,11 +64,12 @@ func GenerateContainersReadyCondition(spec *v1.PodSpec, containerStatuses []v1.C

// If all containers are known and succeeded, just return PodCompleted.
if podPhase == v1.PodSucceeded && len(unknownContainers) == 0 {
return v1.PodCondition{
Type: v1.ContainersReady,
Status: v1.ConditionFalse,
Reason: PodCompleted,
}
return generateContainersReadyConditionForTerminalPhase(podPhase)
}

// If the pod phase is failed, explicitly set the ready condition to false for containers since they may be in progress of terminating.
if podPhase == v1.PodFailed {
return generateContainersReadyConditionForTerminalPhase(podPhase)
}

// Generate message for containers in unknown condition.
Expand Down Expand Up @@ -191,3 +194,33 @@ func GeneratePodInitializedCondition(spec *v1.PodSpec, containerStatuses []v1.Co
Status: v1.ConditionTrue,
}
}

func generateContainersReadyConditionForTerminalPhase(podPhase v1.PodPhase) v1.PodCondition {
condition := v1.PodCondition{
Type: v1.ContainersReady,
Status: v1.ConditionFalse,
}

if podPhase == v1.PodFailed {
condition.Reason = PodFailed
} else if podPhase == v1.PodSucceeded {
condition.Reason = PodCompleted
}

return condition
}

func generatePodReadyConditionForTerminalPhase(podPhase v1.PodPhase) v1.PodCondition {
condition := v1.PodCondition{
Type: v1.PodReady,
Status: v1.ConditionFalse,
}

if podPhase == v1.PodFailed {
condition.Reason = PodFailed
} else if podPhase == v1.PodSucceeded {
condition.Reason = PodCompleted
}

return condition
}
14 changes: 14 additions & 0 deletions pkg/kubelet/status/status_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,20 @@ func mergePodStatus(oldPodStatus, newPodStatus v1.PodStatus, couldHaveRunningCon
}
}

// If the new phase is terminal, explicitly set the ready condition to false for v1.PodReady and v1.ContainersReady.
// It may take some time for kubelet to reconcile the ready condition, so explicitly set ready conditions to false if the phase is terminal.
// This is done to ensure kubelet does not report a status update with terminal pod phase and ready=true.
// See https://issues.k8s.io/108594 for more details.
if podutil.IsPodPhaseTerminal(newPodStatus.Phase) {
if podutil.IsPodReadyConditionTrue(newPodStatus) || podutil.IsContainersReadyConditionTrue(newPodStatus) {
containersReadyCondition := generateContainersReadyConditionForTerminalPhase(newPodStatus.Phase)
podutil.UpdatePodCondition(&newPodStatus, &containersReadyCondition)

podReadyCondition := generatePodReadyConditionForTerminalPhase(newPodStatus.Phase)
podutil.UpdatePodCondition(&newPodStatus, &podReadyCondition)
}
}

return newPodStatus
}

Expand Down
6 changes: 5 additions & 1 deletion pkg/kubelet/status/status_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1548,7 +1548,11 @@ func TestMergePodStatus(t *testing.T) {
Conditions: []v1.PodCondition{
{
Type: v1.PodReady,
Status: v1.ConditionTrue,
Status: v1.ConditionFalse,
},
{
Type: v1.ContainersReady,
Status: v1.ConditionFalse,
},
{
Type: v1.PodScheduled,
Expand Down
43 changes: 34 additions & 9 deletions test/e2e_node/node_shutdown_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ import (

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/tools/cache"
watchtools "k8s.io/client-go/tools/watch"
"k8s.io/kubectl/pkg/util/podutils"

admissionapi "k8s.io/pod-security-admission/api"

"github.com/onsi/ginkgo"
Expand All @@ -41,6 +45,7 @@ import (
v1 "k8s.io/api/core/v1"
schedulingv1 "k8s.io/api/scheduling/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/kubernetes/pkg/features"
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
kubelettypes "k8s.io/kubernetes/pkg/kubelet/types"
Expand Down Expand Up @@ -108,6 +113,34 @@ var _ = SIGDescribe("GracefulNodeShutdown [Serial] [NodeFeature:GracefulNodeShut
framework.ExpectNoError(err)
framework.ExpectEqual(len(list.Items), len(pods), "the number of pods is not as expected")

ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
defer ginkgo.GinkgoRecover()
w := &cache.ListWatch{
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
return f.ClientSet.CoreV1().Pods(f.Namespace.Name).Watch(context.TODO(), options)
},
}

// Setup watch to continuously monitor any pod events and detect invalid pod status updates
_, err = watchtools.Until(ctx, list.ResourceVersion, w, func(event watch.Event) (bool, error) {
if pod, ok := event.Object.(*v1.Pod); ok {
if isPodStatusAffectedByIssue108594(pod) {
return false, fmt.Errorf("failing test due to detecting invalid pod status")
}
// Watch will never terminate (only when the test ends due to context cancellation)
return false, nil
}
return false, nil
})

// Ignore timeout error since the context will be explicitly cancelled and the watch will never return true
if err != nil && err != wait.ErrWaitTimeout {
framework.Failf("watch for invalid pod status failed: %v", err.Error())
}
}()

ginkgo.By("Verifying batch pods are running")
for _, pod := range list.Items {
if podReady, err := testutils.PodRunningReady(&pod); err != nil || !podReady {
Expand All @@ -131,11 +164,6 @@ var _ = SIGDescribe("GracefulNodeShutdown [Serial] [NodeFeature:GracefulNodeShut
framework.ExpectEqual(len(list.Items), len(pods), "the number of pods is not as expected")

for _, pod := range list.Items {
if isPodStatusAffectedByIssue108594(&pod) {
framework.Logf("Detected invalid pod state for pod %q: pod status: %+v", pod.Name, pod.Status)
framework.Failf("failing test due to detecting invalid pod status")
}

if kubelettypes.IsCriticalPod(&pod) {
if isPodShutdown(&pod) {
framework.Logf("Expecting critical pod to be running, but it's not currently. Pod: %q, Pod Status %+v", pod.Name, pod.Status)
Expand Down Expand Up @@ -163,10 +191,6 @@ var _ = SIGDescribe("GracefulNodeShutdown [Serial] [NodeFeature:GracefulNodeShut
framework.ExpectEqual(len(list.Items), len(pods), "the number of pods is not as expected")

for _, pod := range list.Items {
if isPodStatusAffectedByIssue108594(&pod) {
framework.Logf("Detected invalid pod state for pod %q: pod status: %+v", pod.Name, pod.Status)
framework.Failf("failing test due to detecting invalid pod status")
}
if !isPodShutdown(&pod) {
framework.Logf("Expecting pod to be shutdown, but it's not currently: Pod: %q, Pod Status %+v", pod.Name, pod.Status)
return fmt.Errorf("pod should be shutdown, phase: %s", pod.Status.Phase)
Expand All @@ -177,6 +201,7 @@ var _ = SIGDescribe("GracefulNodeShutdown [Serial] [NodeFeature:GracefulNodeShut
// Critical pod starts shutdown after (nodeShutdownGracePeriod-nodeShutdownGracePeriodCriticalPods)
podStatusUpdateTimeout+(nodeShutdownGracePeriod-nodeShutdownGracePeriodCriticalPods),
pollInterval).Should(gomega.BeNil())

})

ginkgo.It("should be able to handle a cancelled shutdown", func() {
Expand Down

0 comments on commit 050f930

Please sign in to comment.