| /* |
| Copyright 2020 The Kubernetes 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 e2enode |
| |
| import ( |
| "context" |
| "fmt" |
| "strconv" |
| "strings" |
| "time" |
| |
| v1 "k8s.io/api/core/v1" |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| admissionapi "k8s.io/pod-security-admission/api" |
| |
| "k8s.io/apimachinery/pkg/fields" |
| "k8s.io/apimachinery/pkg/util/uuid" |
| "k8s.io/kubernetes/pkg/kubelet/events" |
| "k8s.io/kubernetes/test/e2e/feature" |
| "k8s.io/kubernetes/test/e2e/framework" |
| e2eevents "k8s.io/kubernetes/test/e2e/framework/events" |
| e2epod "k8s.io/kubernetes/test/e2e/framework/pod" |
| testutils "k8s.io/kubernetes/test/utils" |
| imageutils "k8s.io/kubernetes/test/utils/image" |
| |
| "k8s.io/kubernetes/pkg/features" |
| kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config" |
| |
| "github.com/onsi/ginkgo/v2" |
| "github.com/onsi/gomega" |
| ) |
| |
| var _ = SIGDescribe("Pod conditions managed by Kubelet", func() { |
| f := framework.NewDefaultFramework("pod-conditions") |
| f.NamespacePodSecurityLevel = admissionapi.LevelBaseline |
| |
| f.Context("including PodReadyToStartContainers condition", f.WithSerial(), feature.PodReadyToStartContainersCondition, func() { |
| tempSetCurrentKubeletConfig(f, func(ctx context.Context, initialConfig *kubeletconfig.KubeletConfiguration) { |
| initialConfig.FeatureGates = map[string]bool{ |
| string(features.PodReadyToStartContainersCondition): true, |
| } |
| }) |
| ginkgo.It("a pod without init containers should report all conditions set in expected order after the pod is up", runPodReadyConditionsTest(f, false, true)) |
| ginkgo.It("a pod with init containers should report all conditions set in expected order after the pod is up", runPodReadyConditionsTest(f, true, true)) |
| ginkgo.It("a pod failing to mount volumes and without init containers should report scheduled and initialized conditions set", runPodFailingConditionsTest(f, false, true)) |
| ginkgo.It("a pod failing to mount volumes and with init containers should report just the scheduled condition set", runPodFailingConditionsTest(f, true, true)) |
| cleanupPods(f) |
| }) |
| |
| ginkgo.Context("without PodReadyToStartContainersCondition condition", func() { |
| ginkgo.It("a pod without init containers should report all conditions set in expected order after the pod is up", runPodReadyConditionsTest(f, false, false)) |
| ginkgo.It("a pod with init containers should report all conditions set in expected order after the pod is up", runPodReadyConditionsTest(f, true, false)) |
| ginkgo.It("a pod failing to mount volumes and without init containers should report scheduled and initialized conditions set", runPodFailingConditionsTest(f, false, false)) |
| ginkgo.It("a pod failing to mount volumes and with init containers should report just the scheduled condition set", runPodFailingConditionsTest(f, true, false)) |
| cleanupPods(f) |
| }) |
| }) |
| |
| func runPodFailingConditionsTest(f *framework.Framework, hasInitContainers, checkPodReadyToStart bool) func(ctx context.Context) { |
| return func(ctx context.Context) { |
| ginkgo.By("creating a pod whose sandbox creation is blocked due to a missing volume") |
| |
| p := webserverPodSpec("pod-"+string(uuid.NewUUID()), "web1", "init1", hasInitContainers) |
| p.Spec.Volumes = []v1.Volume{ |
| { |
| Name: "cm", |
| VolumeSource: v1.VolumeSource{ |
| ConfigMap: &v1.ConfigMapVolumeSource{ |
| LocalObjectReference: v1.LocalObjectReference{Name: "does-not-exist"}, |
| }, |
| }, |
| }, |
| } |
| p.Spec.Containers[0].VolumeMounts = []v1.VolumeMount{ |
| { |
| Name: "cm", |
| MountPath: "/config", |
| }, |
| } |
| |
| p = e2epod.NewPodClient(f).Create(ctx, p) |
| |
| ginkgo.By("waiting until kubelet has started trying to set up the pod and started to fail") |
| |
| eventSelector := fields.Set{ |
| "involvedObject.kind": "Pod", |
| "involvedObject.name": p.Name, |
| "involvedObject.namespace": f.Namespace.Name, |
| "reason": events.FailedMountVolume, |
| }.AsSelector().String() |
| framework.ExpectNoError(e2eevents.WaitTimeoutForEvent(ctx, f.ClientSet, f.Namespace.Name, eventSelector, "MountVolume.SetUp failed for volume", framework.PodEventTimeout)) |
| |
| p, err := e2epod.NewPodClient(f).Get(ctx, p.Name, metav1.GetOptions{}) |
| framework.ExpectNoError(err) |
| |
| ginkgo.By("checking pod condition for a pod whose sandbox creation is blocked") |
| |
| scheduledTime, err := getTransitionTimeForPodConditionWithStatus(p, v1.PodScheduled, true) |
| framework.ExpectNoError(err) |
| |
| // Verify PodReadyToStartContainers is not set (since sandboxcreation is blocked) |
| if checkPodReadyToStart { |
| _, err := getTransitionTimeForPodConditionWithStatus(p, v1.PodReadyToStartContainers, false) |
| framework.ExpectNoError(err) |
| } |
| |
| if hasInitContainers { |
| // Verify PodInitialized is not set if init containers are present (since sandboxcreation is blocked) |
| _, err := getTransitionTimeForPodConditionWithStatus(p, v1.PodInitialized, false) |
| framework.ExpectNoError(err) |
| } else { |
| // Verify PodInitialized is set if init containers are not present (since without init containers, it gets set very early) |
| initializedTime, err := getTransitionTimeForPodConditionWithStatus(p, v1.PodInitialized, true) |
| framework.ExpectNoError(err) |
| gomega.Expect(initializedTime.Before(scheduledTime)).NotTo(gomega.BeTrue(), fmt.Sprintf("pod without init containers is initialized at: %v which is before pod scheduled at: %v", initializedTime, scheduledTime)) |
| } |
| |
| // Verify ContainersReady is not set (since sandboxcreation is blocked) |
| _, err = getTransitionTimeForPodConditionWithStatus(p, v1.ContainersReady, false) |
| framework.ExpectNoError(err) |
| // Verify PodReady is not set (since sandboxcreation is blocked) |
| _, err = getTransitionTimeForPodConditionWithStatus(p, v1.PodReady, false) |
| framework.ExpectNoError(err) |
| |
| // this testcase is creating the missing volume that unblock the pod above, |
| // and check PodReadyToStartContainer is setting correctly. |
| ginkgo.By("checking pod condition for a pod when volumes source is created") |
| |
| configmap := v1.ConfigMap{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: "cm-that-unblock-pod-condition", |
| }, |
| Data: map[string]string{ |
| "key": "value", |
| }, |
| BinaryData: map[string][]byte{ |
| "binaryKey": []byte("value"), |
| }, |
| } |
| |
| _, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Create(ctx, &configmap, metav1.CreateOptions{}) |
| framework.ExpectNoError(err) |
| |
| defer func() { |
| err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Delete(ctx, "cm-that-unblock-pod-condition", metav1.DeleteOptions{}) |
| framework.ExpectNoError(err, "unable to delete configmap") |
| }() |
| |
| p2 := webserverPodSpec("pod2-"+string(uuid.NewUUID()), "web2", "init2", hasInitContainers) |
| p2.Spec.Volumes = []v1.Volume{ |
| { |
| Name: "cm-2", |
| VolumeSource: v1.VolumeSource{ |
| ConfigMap: &v1.ConfigMapVolumeSource{ |
| LocalObjectReference: v1.LocalObjectReference{Name: "cm-that-unblock-pod-condition"}, |
| }, |
| }, |
| }, |
| } |
| p2.Spec.Containers[0].VolumeMounts = []v1.VolumeMount{ |
| { |
| Name: "cm-2", |
| MountPath: "/config", |
| }, |
| } |
| |
| p2 = e2epod.NewPodClient(f).Create(ctx, p2) |
| framework.ExpectNoError(e2epod.WaitTimeoutForPodReadyInNamespace(ctx, f.ClientSet, p2.Name, p2.Namespace, framework.PodStartTimeout)) |
| |
| p2, err = e2epod.NewPodClient(f).Get(ctx, p2.Name, metav1.GetOptions{}) |
| framework.ExpectNoError(err) |
| |
| _, err = getTransitionTimeForPodConditionWithStatus(p2, v1.PodScheduled, true) |
| framework.ExpectNoError(err) |
| |
| _, err = getTransitionTimeForPodConditionWithStatus(p2, v1.PodInitialized, true) |
| framework.ExpectNoError(err) |
| |
| // Verify PodReadyToStartContainers is set (since sandboxcreation is unblocked) |
| if checkPodReadyToStart { |
| _, err = getTransitionTimeForPodConditionWithStatus(p2, v1.PodReadyToStartContainers, true) |
| framework.ExpectNoError(err) |
| } |
| } |
| } |
| |
| func runPodReadyConditionsTest(f *framework.Framework, hasInitContainers, checkPodReadyToStart bool) func(ctx context.Context) { |
| return func(ctx context.Context) { |
| ginkgo.By("creating a pod that successfully comes up in a ready/running state") |
| |
| p := e2epod.NewPodClient(f).Create(ctx, webserverPodSpec("pod-"+string(uuid.NewUUID()), "web1", "init1", hasInitContainers)) |
| framework.ExpectNoError(e2epod.WaitTimeoutForPodReadyInNamespace(ctx, f.ClientSet, p.Name, f.Namespace.Name, framework.PodStartTimeout)) |
| |
| p, err := e2epod.NewPodClient(f).Get(ctx, p.Name, metav1.GetOptions{}) |
| framework.ExpectNoError(err) |
| isReady, err := testutils.PodRunningReady(p) |
| framework.ExpectNoError(err) |
| if !isReady { |
| framework.Failf("pod %q should be ready", p.Name) |
| } |
| |
| ginkgo.By("checking order of pod condition transitions for a pod with no container/sandbox restarts") |
| |
| scheduledTime, err := getTransitionTimeForPodConditionWithStatus(p, v1.PodScheduled, true) |
| framework.ExpectNoError(err) |
| initializedTime, err := getTransitionTimeForPodConditionWithStatus(p, v1.PodInitialized, true) |
| framework.ExpectNoError(err) |
| |
| condBeforeContainersReadyTransitionTime := initializedTime |
| errSubstrIfContainersReadyTooEarly := "is initialized" |
| if checkPodReadyToStart { |
| readyToStartContainersTime, err := getTransitionTimeForPodConditionWithStatus(p, v1.PodReadyToStartContainers, true) |
| framework.ExpectNoError(err) |
| |
| if hasInitContainers { |
| // With init containers, verify the sequence of conditions is: Scheduled => PodReadyToStartContainers => Initialized |
| gomega.Expect(readyToStartContainersTime.Before(scheduledTime)).ToNot(gomega.BeTrue(), fmt.Sprintf("pod with init containers is initialized at: %v which is before pod has ready to start at: %v", initializedTime, readyToStartContainersTime)) |
| gomega.Expect(initializedTime.Before(readyToStartContainersTime)).ToNot(gomega.BeTrue(), fmt.Sprintf("pod with init containers is initialized at: %v which is before pod has ready to start at: %v", initializedTime, readyToStartContainersTime)) |
| } else { |
| // Without init containers, verify the sequence of conditions is: Scheduled => Initialized => PodReadyToStartContainers |
| condBeforeContainersReadyTransitionTime = readyToStartContainersTime |
| errSubstrIfContainersReadyTooEarly = "ready to start" |
| gomega.Expect(initializedTime.Before(scheduledTime)).NotTo(gomega.BeTrue(), fmt.Sprintf("pod without init containers initialized at: %v which is before pod scheduled at: %v", initializedTime, scheduledTime)) |
| gomega.Expect(readyToStartContainersTime.Before(initializedTime)).NotTo(gomega.BeTrue(), fmt.Sprintf("pod without init containers has ready to start at: %v which is before pod is initialized at: %v", readyToStartContainersTime, initializedTime)) |
| } |
| } else { |
| // In the absence of PodHasReadyToStartContainers feature disabled, verify the sequence is: Scheduled => Initialized |
| gomega.Expect(initializedTime.Before(scheduledTime)).NotTo(gomega.BeTrue(), fmt.Sprintf("pod initialized at: %v which is before pod scheduled at: %v", initializedTime, scheduledTime)) |
| } |
| // Verify the next condition to get set is ContainersReady |
| containersReadyTime, err := getTransitionTimeForPodConditionWithStatus(p, v1.ContainersReady, true) |
| framework.ExpectNoError(err) |
| gomega.Expect(containersReadyTime.Before(condBeforeContainersReadyTransitionTime)).NotTo(gomega.BeTrue(), fmt.Sprintf("containers ready at: %v which is before pod %s: %v", containersReadyTime, errSubstrIfContainersReadyTooEarly, initializedTime)) |
| |
| // Verify ContainersReady => PodReady |
| podReadyTime, err := getTransitionTimeForPodConditionWithStatus(p, v1.PodReady, true) |
| framework.ExpectNoError(err) |
| gomega.Expect(podReadyTime.Before(containersReadyTime)).NotTo(gomega.BeTrue(), fmt.Sprintf("pod ready at: %v which is before pod containers ready at: %v", podReadyTime, containersReadyTime)) |
| } |
| } |
| |
| func getTransitionTimeForPodConditionWithStatus(pod *v1.Pod, condType v1.PodConditionType, expectedStatus bool) (time.Time, error) { |
| for _, cond := range pod.Status.Conditions { |
| if cond.Type == condType { |
| if strings.EqualFold(string(cond.Status), strconv.FormatBool(expectedStatus)) { |
| return cond.LastTransitionTime.Time, nil |
| } |
| return time.Time{}, fmt.Errorf("condition: %s found for pod but status: %s did not match expected status: %s", condType, cond.Status, strconv.FormatBool(expectedStatus)) |
| } |
| } |
| return time.Time{}, fmt.Errorf("condition: %s not found for pod", condType) |
| } |
| |
| func webserverPodSpec(podName, containerName, initContainerName string, addInitContainer bool) *v1.Pod { |
| p := &v1.Pod{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: podName, |
| }, |
| Spec: v1.PodSpec{ |
| Containers: []v1.Container{ |
| { |
| Name: containerName, |
| Image: imageutils.GetE2EImage(imageutils.Agnhost), |
| Args: []string{"test-webserver"}, |
| }, |
| }, |
| }, |
| } |
| if addInitContainer { |
| p.Spec.InitContainers = []v1.Container{ |
| { |
| Name: initContainerName, |
| Image: imageutils.GetE2EImage(imageutils.BusyBox), |
| Command: []string{"sh", "-c", "sleep 5s"}, |
| }, |
| } |
| } |
| return p |
| } |