| /* |
| Copyright 2023 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" |
| "strings" |
| "time" |
| |
| v1 "k8s.io/api/core/v1" |
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| "k8s.io/apimachinery/pkg/util/uuid" |
| "k8s.io/kubernetes/test/e2e/framework" |
| e2epod "k8s.io/kubernetes/test/e2e/framework/pod" |
| imageutils "k8s.io/kubernetes/test/utils/image" |
| admissionapi "k8s.io/pod-security-admission/api" |
| |
| "github.com/onsi/ginkgo/v2" |
| "github.com/onsi/gomega" |
| ) |
| |
| const ( |
| testFinalizer = "example.com/test-finalizer" |
| ) |
| |
| var _ = SIGDescribe("Deleted pods handling", framework.WithNodeConformance(), func() { |
| f := framework.NewDefaultFramework("deleted-pods-test") |
| f.NamespacePodSecurityLevel = admissionapi.LevelBaseline |
| |
| ginkgo.It("Should transition to Failed phase a pod which is deleted while pending", func(ctx context.Context) { |
| podName := "deleted-pending-" + string(uuid.NewUUID()) |
| podSpec := &v1.Pod{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: podName, |
| Finalizers: []string{testFinalizer}, |
| }, |
| Spec: v1.PodSpec{ |
| RestartPolicy: v1.RestartPolicyAlways, |
| Containers: []v1.Container{ |
| { |
| Name: podName, |
| Image: "non-existing-repo/non-existing-image:v1.0", |
| ImagePullPolicy: "Always", |
| Command: []string{"bash"}, |
| Args: []string{"-c", `echo "Hello world"`}, |
| }, |
| }, |
| }, |
| } |
| ginkgo.By("creating the pod with invalid image reference and finalizer") |
| pod := e2epod.NewPodClient(f).Create(ctx, podSpec) |
| |
| ginkgo.By("set up cleanup of the finalizer") |
| ginkgo.DeferCleanup(e2epod.NewPodClient(f).RemoveFinalizer, pod.Name, testFinalizer) |
| |
| ginkgo.By("Waiting for the pod to be scheduled so that kubelet owns it") |
| err := e2epod.WaitForPodScheduled(ctx, f.ClientSet, pod.Namespace, pod.Name) |
| framework.ExpectNoError(err, "Failed to await for the pod to be scheduled: %q", pod.Name) |
| |
| ginkgo.By(fmt.Sprintf("Deleting the pod (%v/%v) to set a deletion timestamp", pod.Namespace, pod.Name)) |
| err = e2epod.NewPodClient(f).Delete(ctx, pod.Name, metav1.DeleteOptions{}) |
| framework.ExpectNoError(err, "Failed to delete the pod: %q", pod.Name) |
| |
| ginkgo.By(fmt.Sprintf("Waiting for the pod (%v/%v) to be transitioned into the Failed phase", pod.Namespace, pod.Name)) |
| err = e2epod.WaitForPodTerminatedInNamespace(ctx, f.ClientSet, pod.Name, "", f.Namespace.Name) |
| framework.ExpectNoError(err, "Failed to await for the pod to be terminated: %q", pod.Name) |
| |
| ginkgo.By(fmt.Sprintf("Fetch the end state of the pod (%v/%v)", pod.Namespace, pod.Name)) |
| pod, err = e2epod.NewPodClient(f).Get(ctx, pod.Name, metav1.GetOptions{}) |
| framework.ExpectNoError(err, "Failed to fetch the end state of the pod: %q", pod.Name) |
| }) |
| |
| ginkgo.DescribeTable("Should transition to Failed phase a deleted pod if non-zero exit codes", |
| func(ctx context.Context, policy v1.RestartPolicy) { |
| podName := "deleted-running-" + strings.ToLower(string(policy)) + "-" + string(uuid.NewUUID()) |
| podSpec := e2epod.MustMixinRestrictedPodSecurity(&v1.Pod{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: podName, |
| Finalizers: []string{testFinalizer}, |
| }, |
| Spec: v1.PodSpec{ |
| RestartPolicy: policy, |
| Containers: []v1.Container{ |
| { |
| Name: podName, |
| Image: imageutils.GetE2EImage(imageutils.BusyBox), |
| Command: []string{"sleep", "1800"}, |
| }, |
| }, |
| }, |
| }) |
| ginkgo.By(fmt.Sprintf("Creating a pod (%v/%v) with restart policy: %v", f.Namespace.Name, podSpec.Name, podSpec.Spec.RestartPolicy)) |
| pod := e2epod.NewPodClient(f).Create(ctx, podSpec) |
| |
| ginkgo.By("set up cleanup of the finalizer") |
| ginkgo.DeferCleanup(e2epod.NewPodClient(f).RemoveFinalizer, pod.Name, testFinalizer) |
| |
| ginkgo.By("Waiting for the pod to be running") |
| err := e2epod.WaitForPodNameRunningInNamespace(ctx, f.ClientSet, pod.Name, f.Namespace.Name) |
| framework.ExpectNoError(err, "Failed to await for the pod to be running: %q", pod.Name) |
| |
| ginkgo.By(fmt.Sprintf("Deleting the pod (%v/%v) to set a deletion timestamp", pod.Namespace, pod.Name)) |
| err = e2epod.NewPodClient(f).Delete(ctx, pod.Name, *metav1.NewDeleteOptions(1)) |
| framework.ExpectNoError(err, "Failed to delete the pod: %q", pod.Name) |
| |
| ginkgo.By(fmt.Sprintf("Waiting for the pod (%v/%v) to be transitioned to the failed phase", pod.Namespace, pod.Name)) |
| err = e2epod.WaitForPodTerminatedInNamespace(ctx, f.ClientSet, pod.Name, "", f.Namespace.Name) |
| framework.ExpectNoError(err, "Failed to await for the pod to be terminated: %q", pod.Name) |
| |
| ginkgo.By(fmt.Sprintf("Fetching the end state of the pod (%v/%v)", pod.Namespace, pod.Name)) |
| pod, err = e2epod.NewPodClient(f).Get(ctx, pod.Name, metav1.GetOptions{}) |
| framework.ExpectNoError(err, "Failed to fetch the end state of the pod: %q", pod.Name) |
| |
| ginkgo.By(fmt.Sprintf("Verify the pod (%v/%v) container is in the terminated state", pod.Namespace, pod.Name)) |
| gomega.Expect(pod.Status.ContainerStatuses).Should(gomega.HaveLen(1)) |
| containerStatus := pod.Status.ContainerStatuses[0] |
| gomega.Expect(containerStatus.State.Terminated).ToNot(gomega.BeNil(), "The pod container is in not in the Terminated state") |
| |
| ginkgo.By(fmt.Sprintf("Verify the pod (%v/%v) container exit code is 137", pod.Namespace, pod.Name)) |
| gomega.Expect(containerStatus.State.Terminated.ExitCode).Should(gomega.Equal(int32(137))) |
| }, |
| ginkgo.Entry("Restart policy Always", v1.RestartPolicyAlways), |
| ginkgo.Entry("Restart policy OnFailure", v1.RestartPolicyOnFailure), |
| ginkgo.Entry("Restart policy Never", v1.RestartPolicyNever), |
| ) |
| |
| ginkgo.DescribeTable("Should transition to Succeeded phase a deleted pod when containers complete with 0 exit code", |
| func(ctx context.Context, policy v1.RestartPolicy) { |
| podName := "deleted-running-" + strings.ToLower(string(policy)) + "-" + string(uuid.NewUUID()) |
| podSpec := e2epod.MustMixinRestrictedPodSecurity(&v1.Pod{ |
| ObjectMeta: metav1.ObjectMeta{ |
| Name: podName, |
| Finalizers: []string{testFinalizer}, |
| }, |
| Spec: v1.PodSpec{ |
| RestartPolicy: policy, |
| Containers: []v1.Container{ |
| { |
| Name: podName, |
| Image: imageutils.GetE2EImage(imageutils.BusyBox), |
| Command: []string{"sh", "-c"}, |
| Args: []string{` |
| sleep 9999999 & |
| PID=$! |
| _term() { |
| kill $PID |
| echo "Caught SIGTERM signal!" |
| } |
| |
| trap _term SIGTERM |
| wait $PID |
| |
| exit 0 |
| `, |
| }, |
| }, |
| }, |
| }, |
| }) |
| ginkgo.By(fmt.Sprintf("Creating a pod (%v/%v) with restart policy: %v", f.Namespace.Name, podSpec.Name, podSpec.Spec.RestartPolicy)) |
| pod := e2epod.NewPodClient(f).Create(ctx, podSpec) |
| |
| ginkgo.By("set up cleanup of the finalizer") |
| ginkgo.DeferCleanup(e2epod.NewPodClient(f).RemoveFinalizer, pod.Name, testFinalizer) |
| |
| ginkgo.By("Waiting for the pod to be running") |
| err := e2epod.WaitForPodNameRunningInNamespace(ctx, f.ClientSet, pod.Name, f.Namespace.Name) |
| framework.ExpectNoError(err, "Failed to await for the pod to be running: %q", pod.Name) |
| |
| ginkgo.By(fmt.Sprintf("Deleting the pod (%v/%v) to set a deletion timestamp", pod.Namespace, pod.Name)) |
| // wait a little bit to make sure the we are inside the while and that the trap is registered |
| time.Sleep(time.Second) |
| err = e2epod.NewPodClient(f).Delete(ctx, pod.Name, metav1.DeleteOptions{}) |
| framework.ExpectNoError(err, "Failed to delete the pod: %q", pod.Name) |
| |
| ginkgo.By(fmt.Sprintf("Waiting for the pod (%v/%v) to be transitioned to the succeeded phase", pod.Namespace, pod.Name)) |
| err = e2epod.WaitForPodSuccessInNamespace(ctx, f.ClientSet, pod.Name, f.Namespace.Name) |
| framework.ExpectNoError(err, "Failed to await for the pod to be succeeded: %q", pod.Name) |
| |
| ginkgo.By(fmt.Sprintf("Fetching the end state of the pod (%v/%v)", pod.Namespace, pod.Name)) |
| pod, err = e2epod.NewPodClient(f).Get(ctx, pod.Name, metav1.GetOptions{}) |
| framework.ExpectNoError(err, "Failed to fetch the end state of the pod: %q", pod.Name) |
| |
| ginkgo.By(fmt.Sprintf("Verify the pod (%v/%v) container is in the terminated state", pod.Namespace, pod.Name)) |
| gomega.Expect(pod.Status.ContainerStatuses).Should(gomega.HaveLen(1)) |
| containerStatus := pod.Status.ContainerStatuses[0] |
| gomega.Expect(containerStatus.State.Terminated).ShouldNot(gomega.BeNil(), "The pod container is in not in the Terminated state") |
| |
| ginkgo.By(fmt.Sprintf("Verifying the exit code for the terminated container is 0 for pod (%v/%v)", pod.Namespace, pod.Name)) |
| gomega.Expect(containerStatus.State.Terminated.ExitCode).Should(gomega.Equal(int32(0))) |
| }, |
| ginkgo.Entry("Restart policy Always", v1.RestartPolicyAlways), |
| ginkgo.Entry("Restart policy OnFailure", v1.RestartPolicyOnFailure), |
| ginkgo.Entry("Restart policy Never", v1.RestartPolicyNever), |
| ) |
| |
| }) |