kubernetes/test/e2e_node/node_shutdown_linux_test.go

688 lines
24 KiB
Go
Raw Normal View History

//go:build linux
2021-02-01 02:32:41 -05:00
/*
Copyright 2021 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"
"os"
"os/exec"
"regexp"
"strconv"
2021-02-01 02:32:41 -05:00
"time"
2021-11-25 01:24:32 -05:00
apierrors "k8s.io/apimachinery/pkg/api/errors"
2021-02-01 02:32:41 -05:00
"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"
2021-02-01 02:32:41 -05:00
"github.com/onsi/ginkgo/v2"
2021-02-01 02:32:41 -05:00
"github.com/onsi/gomega"
"k8s.io/kubernetes/pkg/apis/scheduling"
"k8s.io/kubernetes/test/e2e/feature"
"k8s.io/kubernetes/test/e2e/framework"
e2e: adapt to moved code This is the result of automatically editing source files like this: go install golang.org/x/tools/cmd/goimports@latest find ./test/e2e* -name "*.go" | xargs env PATH=$GOPATH/bin:$PATH ./e2e-framework-sed.sh with e2e-framework-sed.sh containing this: sed -i \ -e "s/\(f\|fr\|\w\w*\.[fF]\w*\)\.ExecCommandInContainer(/e2epod.ExecCommandInContainer(\1, /" \ -e "s/\(f\|fr\|\w\w*\.[fF]\w*\)\.ExecCommandInContainerWithFullOutput(/e2epod.ExecCommandInContainerWithFullOutput(\1, /" \ -e "s/\(f\|fr\|\w\w*\.[fF]\w*\)\.ExecShellInContainer(/e2epod.ExecShellInContainer(\1, /" \ -e "s/\(f\|fr\|\w\w*\.[fF]\w*\)\.ExecShellInPod(/e2epod.ExecShellInPod(\1, /" \ -e "s/\(f\|fr\|\w\w*\.[fF]\w*\)\.ExecShellInPodWithFullOutput(/e2epod.ExecShellInPodWithFullOutput(\1, /" \ -e "s/\(f\|fr\|\w\w*\.[fF]\w*\)\.ExecWithOptions(/e2epod.ExecWithOptions(\1, /" \ -e "s/\(f\|fr\|\w\w*\.[fF]\w*\)\.MatchContainerOutput(/e2eoutput.MatchContainerOutput(\1, /" \ -e "s/\(f\|fr\|\w\w*\.[fF]\w*\)\.PodClient(/e2epod.NewPodClient(\1, /" \ -e "s/\(f\|fr\|\w\w*\.[fF]\w*\)\.PodClientNS(/e2epod.PodClientNS(\1, /" \ -e "s/\(f\|fr\|\w\w*\.[fF]\w*\)\.TestContainerOutput(/e2eoutput.TestContainerOutput(\1, /" \ -e "s/\(f\|fr\|\w\w*\.[fF]\w*\)\.TestContainerOutputRegexp(/e2eoutput.TestContainerOutputRegexp(\1, /" \ -e "s/framework.AddOrUpdateLabelOnNode\b/e2enode.AddOrUpdateLabelOnNode/" \ -e "s/framework.AllNodes\b/e2edebug.AllNodes/" \ -e "s/framework.AllNodesReady\b/e2enode.AllNodesReady/" \ -e "s/framework.ContainerResourceGatherer\b/e2edebug.ContainerResourceGatherer/" \ -e "s/framework.ContainerResourceUsage\b/e2edebug.ContainerResourceUsage/" \ -e "s/framework.CreateEmptyFileOnPod\b/e2eoutput.CreateEmptyFileOnPod/" \ -e "s/framework.DefaultPodDeletionTimeout\b/e2epod.DefaultPodDeletionTimeout/" \ -e "s/framework.DumpAllNamespaceInfo\b/e2edebug.DumpAllNamespaceInfo/" \ -e "s/framework.DumpDebugInfo\b/e2eoutput.DumpDebugInfo/" \ -e "s/framework.DumpNodeDebugInfo\b/e2edebug.DumpNodeDebugInfo/" \ -e "s/framework.EtcdUpgrade\b/e2eproviders.EtcdUpgrade/" \ -e "s/framework.EventsLister\b/e2edebug.EventsLister/" \ -e "s/framework.ExecOptions\b/e2epod.ExecOptions/" \ -e "s/framework.ExpectNodeHasLabel\b/e2enode.ExpectNodeHasLabel/" \ -e "s/framework.ExpectNodeHasTaint\b/e2enode.ExpectNodeHasTaint/" \ -e "s/framework.GCEUpgradeScript\b/e2eproviders.GCEUpgradeScript/" \ -e "s/framework.ImagePrePullList\b/e2epod.ImagePrePullList/" \ -e "s/framework.KubectlBuilder\b/e2ekubectl.KubectlBuilder/" \ -e "s/framework.LocationParamGKE\b/e2eproviders.LocationParamGKE/" \ -e "s/framework.LogSizeDataTimeseries\b/e2edebug.LogSizeDataTimeseries/" \ -e "s/framework.LogSizeGatherer\b/e2edebug.LogSizeGatherer/" \ -e "s/framework.LogsSizeData\b/e2edebug.LogsSizeData/" \ -e "s/framework.LogsSizeDataSummary\b/e2edebug.LogsSizeDataSummary/" \ -e "s/framework.LogsSizeVerifier\b/e2edebug.LogsSizeVerifier/" \ -e "s/framework.LookForStringInLog\b/e2eoutput.LookForStringInLog/" \ -e "s/framework.LookForStringInPodExec\b/e2eoutput.LookForStringInPodExec/" \ -e "s/framework.LookForStringInPodExecToContainer\b/e2eoutput.LookForStringInPodExecToContainer/" \ -e "s/framework.MasterAndDNSNodes\b/e2edebug.MasterAndDNSNodes/" \ -e "s/framework.MasterNodes\b/e2edebug.MasterNodes/" \ -e "s/framework.MasterUpgradeGKE\b/e2eproviders.MasterUpgradeGKE/" \ -e "s/framework.NewKubectlCommand\b/e2ekubectl.NewKubectlCommand/" \ -e "s/framework.NewLogsVerifier\b/e2edebug.NewLogsVerifier/" \ -e "s/framework.NewNodeKiller\b/e2enode.NewNodeKiller/" \ -e "s/framework.NewResourceUsageGatherer\b/e2edebug.NewResourceUsageGatherer/" \ -e "s/framework.NodeHasTaint\b/e2enode.NodeHasTaint/" \ -e "s/framework.NodeKiller\b/e2enode.NodeKiller/" \ -e "s/framework.NodesSet\b/e2edebug.NodesSet/" \ -e "s/framework.PodClient\b/e2epod.PodClient/" \ -e "s/framework.RemoveLabelOffNode\b/e2enode.RemoveLabelOffNode/" \ -e "s/framework.ResourceConstraint\b/e2edebug.ResourceConstraint/" \ -e "s/framework.ResourceGathererOptions\b/e2edebug.ResourceGathererOptions/" \ -e "s/framework.ResourceUsagePerContainer\b/e2edebug.ResourceUsagePerContainer/" \ -e "s/framework.ResourceUsageSummary\b/e2edebug.ResourceUsageSummary/" \ -e "s/framework.RunHostCmd\b/e2eoutput.RunHostCmd/" \ -e "s/framework.RunHostCmdOrDie\b/e2eoutput.RunHostCmdOrDie/" \ -e "s/framework.RunHostCmdWithFullOutput\b/e2eoutput.RunHostCmdWithFullOutput/" \ -e "s/framework.RunHostCmdWithRetries\b/e2eoutput.RunHostCmdWithRetries/" \ -e "s/framework.RunKubectl\b/e2ekubectl.RunKubectl/" \ -e "s/framework.RunKubectlInput\b/e2ekubectl.RunKubectlInput/" \ -e "s/framework.RunKubectlOrDie\b/e2ekubectl.RunKubectlOrDie/" \ -e "s/framework.RunKubectlOrDieInput\b/e2ekubectl.RunKubectlOrDieInput/" \ -e "s/framework.RunKubectlWithFullOutput\b/e2ekubectl.RunKubectlWithFullOutput/" \ -e "s/framework.RunKubemciCmd\b/e2ekubectl.RunKubemciCmd/" \ -e "s/framework.RunKubemciWithKubeconfig\b/e2ekubectl.RunKubemciWithKubeconfig/" \ -e "s/framework.SingleContainerSummary\b/e2edebug.SingleContainerSummary/" \ -e "s/framework.SingleLogSummary\b/e2edebug.SingleLogSummary/" \ -e "s/framework.TimestampedSize\b/e2edebug.TimestampedSize/" \ -e "s/framework.WaitForAllNodesSchedulable\b/e2enode.WaitForAllNodesSchedulable/" \ -e "s/framework.WaitForSSHTunnels\b/e2enode.WaitForSSHTunnels/" \ -e "s/framework.WorkItem\b/e2edebug.WorkItem/" \ "$@" for i in "$@"; do # Import all sub packages and let goimports figure out which of those # are redundant (= already imported) or not needed. sed -i -e '/"k8s.io.kubernetes.test.e2e.framework"/a e2edebug "k8s.io/kubernetes/test/e2e/framework/debug"' "$i" sed -i -e '/"k8s.io.kubernetes.test.e2e.framework"/a e2ekubectl "k8s.io/kubernetes/test/e2e/framework/kubectl"' "$i" sed -i -e '/"k8s.io.kubernetes.test.e2e.framework"/a e2enode "k8s.io/kubernetes/test/e2e/framework/node"' "$i" sed -i -e '/"k8s.io.kubernetes.test.e2e.framework"/a e2eoutput "k8s.io/kubernetes/test/e2e/framework/pod/output"' "$i" sed -i -e '/"k8s.io.kubernetes.test.e2e.framework"/a e2epod "k8s.io/kubernetes/test/e2e/framework/pod"' "$i" sed -i -e '/"k8s.io.kubernetes.test.e2e.framework"/a e2eproviders "k8s.io/kubernetes/test/e2e/framework/providers"' "$i" goimports -w "$i" done
2022-09-08 10:04:17 -04:00
e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
"github.com/godbus/dbus/v5"
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/uuid"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/kubernetes/pkg/features"
2021-02-01 02:32:41 -05:00
kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
kubelettypes "k8s.io/kubernetes/pkg/kubelet/types"
testutils "k8s.io/kubernetes/test/utils"
2021-02-01 02:32:41 -05:00
)
var _ = SIGDescribe("GracefulNodeShutdown", framework.WithSerial(), feature.GracefulNodeShutdown, feature.GracefulNodeShutdownBasedOnPodPriority, func() {
2021-02-01 02:32:41 -05:00
f := framework.NewDefaultFramework("graceful-node-shutdown")
f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
ginkgo.BeforeEach(func() {
if _, err := exec.LookPath("systemd-run"); err == nil {
if version, verr := exec.Command("systemd-run", "--version").Output(); verr == nil {
// sample output from $ systemd-run --version
// systemd 245 (245.4-4ubuntu3.13)
re := regexp.MustCompile(`systemd (\d+)`)
if match := re.FindSubmatch(version); len(match) > 1 {
systemdVersion, err := strconv.Atoi(string(match[1]))
if err != nil {
framework.Logf("failed to parse systemd version with error %v, 'systemd-run --version' output was [%s]", err, version)
} else {
// See comments in issue 107043, this is a known problem for a long time that this feature does not work on older systemd
// https://github.com/kubernetes/kubernetes/issues/107043#issuecomment-997546598
if systemdVersion < 245 {
e2eskipper.Skipf("skipping GracefulNodeShutdown tests as we are running on an old version of systemd : %d", systemdVersion)
}
}
}
}
}
})
f.Context("graceful node shutdown; baseline scenario to verify DisruptionTarget is added", func() {
const (
pollInterval = 1 * time.Second
podStatusUpdateTimeout = 30 * time.Second
nodeStatusUpdateTimeout = 30 * time.Second
nodeShutdownGracePeriod = 30 * time.Second
)
tempSetCurrentKubeletConfig(f, func(ctx context.Context, initialConfig *kubeletconfig.KubeletConfiguration) {
if initialConfig.FeatureGates == nil {
initialConfig.FeatureGates = map[string]bool{}
}
initialConfig.FeatureGates[string(features.GracefulNodeShutdown)] = true
initialConfig.FeatureGates[string(features.GracefulNodeShutdownBasedOnPodPriority)] = false
initialConfig.ShutdownGracePeriod = metav1.Duration{Duration: nodeShutdownGracePeriod}
})
ginkgo.BeforeEach(func(ctx context.Context) {
ginkgo.By("Wait for the node to be ready")
waitForNodeReady(ctx)
})
ginkgo.AfterEach(func() {
ginkgo.By("Emitting Shutdown false signal; cancelling the shutdown")
err := emitSignalPrepareForShutdown(false)
framework.ExpectNoError(err)
})
ginkgo.It("should add the DisruptionTarget pod failure condition to the evicted pods", func(ctx context.Context) {
nodeName := getNodeName(ctx, f)
nodeSelector := fields.Set{
"spec.nodeName": nodeName,
}.AsSelector().String()
// Define test pods
pods := []*v1.Pod{
getGracePeriodOverrideTestPod("pod-to-evict-"+string(uuid.NewUUID()), nodeName, 5, ""),
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ginkgo.By("reating batch pods")
e2epod.NewPodClient(f).CreateBatch(ctx, pods)
list, err := e2epod.NewPodClient(f).List(ctx, metav1.ListOptions{
FieldSelector: nodeSelector,
})
framework.ExpectNoError(err)
gomega.Expect(list.Items).To(gomega.HaveLen(len(pods)), "the number of pods is not as expected")
list, err = e2epod.NewPodClient(f).List(ctx, metav1.ListOptions{
FieldSelector: nodeSelector,
})
if err != nil {
framework.Failf("Failed to start batch pod: %q", err)
}
gomega.Expect(list.Items).To(gomega.HaveLen(len(pods)), "the number of pods is not as expected")
for _, pod := range list.Items {
framework.Logf("Pod (%v/%v) status conditions: %q", pod.Namespace, pod.Name, &pod.Status.Conditions)
}
ginkgo.By("Verifying batch pods are running")
for _, pod := range list.Items {
if podReady, err := testutils.PodRunningReady(&pod); err != nil || !podReady {
framework.Failf("Failed to start batch pod: (%v/%v)", pod.Namespace, pod.Name)
}
}
ginkgo.By("Emitting shutdown signal")
err = emitSignalPrepareForShutdown(true)
framework.ExpectNoError(err)
ginkgo.By("Verifying that all pods are shutdown")
// All pod should be shutdown
gomega.Eventually(func() error {
list, err = e2epod.NewPodClient(f).List(ctx, metav1.ListOptions{
FieldSelector: nodeSelector,
})
if err != nil {
return err
}
gomega.Expect(list.Items).To(gomega.HaveLen(len(pods)), "the number of pods is not as expected")
for _, pod := range list.Items {
if !isPodShutdown(&pod) {
framework.Logf("Expecting pod to be shutdown, but it's not currently. Pod: (%v/%v), Pod Status Phase: %q, Pod Status Reason: %q", pod.Namespace, pod.Name, pod.Status.Phase, pod.Status.Reason)
return fmt.Errorf("pod should be shutdown, phase: %s", pod.Status.Phase)
}
2022-11-08 13:49:13 -05:00
podDisruptionCondition := e2epod.FindPodConditionByType(&pod.Status, v1.DisruptionTarget)
if podDisruptionCondition == nil {
framework.Failf("pod (%v/%v) should have the condition: %q, pod status: %v", pod.Namespace, pod.Name, v1.DisruptionTarget, pod.Status)
}
}
return nil
}, podStatusUpdateTimeout+(nodeShutdownGracePeriod), pollInterval).Should(gomega.BeNil())
})
})
2021-02-01 02:32:41 -05:00
ginkgo.Context("when gracefully shutting down", func() {
const (
pollInterval = 1 * time.Second
podStatusUpdateTimeout = 30 * time.Second
nodeStatusUpdateTimeout = 30 * time.Second
2021-02-01 02:32:41 -05:00
nodeShutdownGracePeriod = 20 * time.Second
nodeShutdownGracePeriodCriticalPods = 10 * time.Second
)
tempSetCurrentKubeletConfig(f, func(ctx context.Context, initialConfig *kubeletconfig.KubeletConfiguration) {
if initialConfig.FeatureGates == nil {
initialConfig.FeatureGates = map[string]bool{}
}
initialConfig.FeatureGates[string(features.GracefulNodeShutdown)] = true
initialConfig.FeatureGates[string(features.GracefulNodeShutdownBasedOnPodPriority)] = false
initialConfig.FeatureGates[string(features.PodReadyToStartContainersCondition)] = true
2021-02-01 02:32:41 -05:00
initialConfig.ShutdownGracePeriod = metav1.Duration{Duration: nodeShutdownGracePeriod}
initialConfig.ShutdownGracePeriodCriticalPods = metav1.Duration{Duration: nodeShutdownGracePeriodCriticalPods}
})
ginkgo.BeforeEach(func(ctx context.Context) {
2021-04-28 04:10:11 -04:00
ginkgo.By("Wait for the node to be ready")
waitForNodeReady(ctx)
2021-04-28 04:10:11 -04:00
})
ginkgo.AfterEach(func(ctx context.Context) {
2021-02-01 02:32:41 -05:00
ginkgo.By("Emitting Shutdown false signal; cancelling the shutdown")
err := emitSignalPrepareForShutdown(false)
framework.ExpectNoError(err)
})
ginkgo.It("should be able to gracefully shutdown pods with various grace periods", func(ctx context.Context) {
nodeName := getNodeName(ctx, f)
2021-02-01 02:32:41 -05:00
nodeSelector := fields.Set{
"spec.nodeName": nodeName,
}.AsSelector().String()
// Define test pods
pods := []*v1.Pod{
getGracePeriodOverrideTestPod("period-120-"+string(uuid.NewUUID()), nodeName, 120, ""),
getGracePeriodOverrideTestPod("period-5-"+string(uuid.NewUUID()), nodeName, 5, ""),
getGracePeriodOverrideTestPod("period-critical-120-"+string(uuid.NewUUID()), nodeName, 120, scheduling.SystemNodeCritical),
getGracePeriodOverrideTestPod("period-critical-5-"+string(uuid.NewUUID()), nodeName, 5, scheduling.SystemNodeCritical),
2021-02-01 02:32:41 -05:00
}
ginkgo.By("Creating batch pods")
e2epod.NewPodClient(f).CreateBatch(ctx, pods)
2021-02-01 02:32:41 -05:00
list, err := e2epod.NewPodClient(f).List(ctx, metav1.ListOptions{
2021-02-01 02:32:41 -05:00
FieldSelector: nodeSelector,
})
framework.ExpectNoError(err)
gomega.Expect(list.Items).To(gomega.HaveLen(len(pods)), "the number of pods is not as expected")
2021-02-01 02:32:41 -05:00
ctx, cancel := context.WithCancel(ctx)
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(ctx, 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 && !wait.Interrupted(err) {
framework.Failf("watch for invalid pod status failed: %v", err.Error())
}
}()
ginkgo.By("Verifying batch pods are running")
2021-02-01 02:32:41 -05:00
for _, pod := range list.Items {
if podReady, err := testutils.PodRunningReady(&pod); err != nil || !podReady {
framework.Failf("Failed to start batch pod: %v", pod.Name)
}
2021-02-01 02:32:41 -05:00
}
ginkgo.By("Emitting shutdown signal")
err = emitSignalPrepareForShutdown(true)
framework.ExpectNoError(err)
ginkgo.By("Verifying that non-critical pods are shutdown")
2021-02-01 02:32:41 -05:00
// Not critical pod should be shutdown
gomega.Eventually(ctx, func(ctx context.Context) error {
list, err = e2epod.NewPodClient(f).List(ctx, metav1.ListOptions{
2021-02-01 02:32:41 -05:00
FieldSelector: nodeSelector,
})
if err != nil {
return err
}
gomega.Expect(list.Items).To(gomega.HaveLen(len(pods)), "the number of pods is not as expected")
2021-02-01 02:32:41 -05:00
for _, pod := range list.Items {
if kubelettypes.IsCriticalPod(&pod) {
if isPodShutdown(&pod) {
framework.Logf("Expecting critical pod (%v/%v) to be running, but it's not currently. Pod Status %+v", pod.Namespace, pod.Name, pod.Status)
return fmt.Errorf("critical pod (%v/%v) should not be shutdown, phase: %s", pod.Namespace, pod.Name, pod.Status.Phase)
2021-02-01 02:32:41 -05:00
}
} else {
if !isPodShutdown(&pod) {
e2e framework: unify logging, support skipping helpers ginkgo.GinkgoHelper is a recent addition to ginkgo which allows functions to mark themselves as helper. This then changes which callstack gets reported for failures. It makes sense to support the same mechanism also for logging. There's also no reason why framework.Logf should produce output that is in a different format than klog log entries. Having time stamps formatted differently makes it hard to read test output which uses a mixture of both. Another user-visible advantage is that the error log entry from framework.ExpectNoError now references the test source code. With textlogger there is a simple replacement for klog that can be reconfigured to let the caller handle stack unwinding. klog itself doesn't support that and should be modified to support it (feature freeze). Emitting printf-style output via that logger would work, but become less readable because the message string would get quoted instead of printing it verbatim as before. So instead, the traditional klog header gets reproduced in the framework code. In this example, the first line is from klog, the second from Logf: I0111 11:00:54.088957 332873 factory.go:193] Registered Plugin "containerd" ... I0111 11:00:54.987534 332873 util.go:506] >>> kubeConfig: /var/run/kubernetes/admin.kubeconfig Indention is a bit different because the initial output is printed before installing the logger which writes through ginkgo.GinkgoWriter. One welcome side effect is that now "go vet" detects mismatched parameters for framework.Logf because fmt.Sprintf is called without mangling the format string. Some of the calls were incorrect.
2024-01-11 06:45:51 -05:00
framework.Logf("Expecting non-critical pod (%v/%v) to be shutdown, but it's not currently. Pod Status %+v", pod.Namespace, pod.Name, pod.Status)
return fmt.Errorf("pod (%v/%v) should be shutdown, phase: %s", pod.Namespace, pod.Name, pod.Status.Phase)
2021-02-01 02:32:41 -05:00
}
}
}
return nil
}, podStatusUpdateTimeout, pollInterval).Should(gomega.Succeed())
2021-02-01 02:32:41 -05:00
ginkgo.By("Verifying that all pods are shutdown")
2021-02-01 02:32:41 -05:00
// All pod should be shutdown
gomega.Eventually(ctx, func(ctx context.Context) error {
list, err = e2epod.NewPodClient(f).List(ctx, metav1.ListOptions{
2021-02-01 02:32:41 -05:00
FieldSelector: nodeSelector,
})
if err != nil {
return err
}
gomega.Expect(list.Items).To(gomega.HaveLen(len(pods)), "the number of pods is not as expected")
2021-02-01 02:32:41 -05:00
for _, pod := range list.Items {
if !isPodShutdown(&pod) {
framework.Logf("Expecting pod (%v/%v) to be shutdown, but it's not currently: Pod Status %+v", pod.Namespace, pod.Name, pod.Status)
return fmt.Errorf("pod (%v/%v) should be shutdown, phase: %s", pod.Namespace, pod.Name, pod.Status.Phase)
2021-02-01 02:32:41 -05:00
}
}
return nil
},
// Critical pod starts shutdown after (nodeShutdownGracePeriod-nodeShutdownGracePeriodCriticalPods)
podStatusUpdateTimeout+(nodeShutdownGracePeriod-nodeShutdownGracePeriodCriticalPods),
pollInterval).Should(gomega.Succeed())
ginkgo.By("Verify that all pod ready to start condition are set to false after terminating")
// All pod ready to start condition should set to false
gomega.Eventually(ctx, func(ctx context.Context) error {
list, err = e2epod.NewPodClient(f).List(ctx, metav1.ListOptions{
FieldSelector: nodeSelector,
})
if err != nil {
return err
}
gomega.Expect(list.Items).To(gomega.HaveLen(len(pods)))
for _, pod := range list.Items {
if !isPodReadyToStartConditionSetToFalse(&pod) {
framework.Logf("Expecting pod (%v/%v) 's ready to start condition set to false, "+
"but it's not currently: Pod Condition %+v", pod.Namespace, pod.Name, pod.Status.Conditions)
return fmt.Errorf("pod (%v/%v) 's ready to start condition should be false, condition: %v, phase: %s",
pod.Namespace, pod.Name, pod.Status.Conditions, pod.Status.Phase)
}
}
return nil
},
).Should(gomega.Succeed())
2021-02-01 02:32:41 -05:00
})
ginkgo.It("should be able to handle a cancelled shutdown", func(ctx context.Context) {
2021-02-01 02:32:41 -05:00
ginkgo.By("Emitting Shutdown signal")
err := emitSignalPrepareForShutdown(true)
framework.ExpectNoError(err)
gomega.Eventually(ctx, func(ctx context.Context) error {
isReady := getNodeReadyStatus(ctx, f)
2021-02-01 02:32:41 -05:00
if isReady {
return fmt.Errorf("node did not become shutdown as expected")
}
return nil
}, nodeStatusUpdateTimeout, pollInterval).Should(gomega.Succeed())
2021-02-01 02:32:41 -05:00
ginkgo.By("Emitting Shutdown false signal; cancelling the shutdown")
err = emitSignalPrepareForShutdown(false)
framework.ExpectNoError(err)
gomega.Eventually(ctx, func(ctx context.Context) error {
isReady := getNodeReadyStatus(ctx, f)
2021-02-01 02:32:41 -05:00
if !isReady {
return fmt.Errorf("node did not recover as expected")
}
return nil
}, nodeStatusUpdateTimeout, pollInterval).Should(gomega.Succeed())
2021-02-01 02:32:41 -05:00
})
})
framework.Context("when gracefully shutting down with Pod priority", framework.WithFlaky(), func() {
const (
2021-11-25 01:24:32 -05:00
pollInterval = 1 * time.Second
podStatusUpdateTimeout = 30 * time.Second
2021-11-25 01:24:32 -05:00
priorityClassesCreateTimeout = 10 * time.Second
)
var (
customClassA = getPriorityClass("custom-class-a", 100000)
customClassB = getPriorityClass("custom-class-b", 10000)
customClassC = getPriorityClass("custom-class-c", 1000)
)
tempSetCurrentKubeletConfig(f, func(ctx context.Context, initialConfig *kubeletconfig.KubeletConfiguration) {
if initialConfig.FeatureGates == nil {
initialConfig.FeatureGates = map[string]bool{}
}
initialConfig.FeatureGates[string(features.GracefulNodeShutdown)] = true
initialConfig.FeatureGates[string(features.GracefulNodeShutdownBasedOnPodPriority)] = true
initialConfig.ShutdownGracePeriodByPodPriority = []kubeletconfig.ShutdownGracePeriodByPodPriority{
{
Priority: scheduling.SystemCriticalPriority,
ShutdownGracePeriodSeconds: int64(podStatusUpdateTimeout / time.Second),
},
{
Priority: customClassA.Value,
ShutdownGracePeriodSeconds: int64(podStatusUpdateTimeout / time.Second),
},
{
Priority: customClassB.Value,
ShutdownGracePeriodSeconds: int64(podStatusUpdateTimeout / time.Second),
},
{
Priority: customClassC.Value,
ShutdownGracePeriodSeconds: int64(podStatusUpdateTimeout / time.Second),
},
{
Priority: scheduling.DefaultPriorityWhenNoDefaultClassExists,
ShutdownGracePeriodSeconds: int64(podStatusUpdateTimeout / time.Second),
},
}
})
ginkgo.BeforeEach(func(ctx context.Context) {
ginkgo.By("Wait for the node to be ready")
waitForNodeReady(ctx)
2021-11-25 01:24:32 -05:00
customClasses := []*schedulingv1.PriorityClass{customClassA, customClassB, customClassC}
for _, customClass := range customClasses {
_, err := f.ClientSet.SchedulingV1().PriorityClasses().Create(ctx, customClass, metav1.CreateOptions{})
2021-11-25 01:24:32 -05:00
if err != nil && !apierrors.IsAlreadyExists(err) {
framework.ExpectNoError(err)
}
}
gomega.Eventually(ctx, func(ctx context.Context) error {
2021-11-25 01:24:32 -05:00
for _, customClass := range customClasses {
_, err := f.ClientSet.SchedulingV1().PriorityClasses().Get(ctx, customClass.Name, metav1.GetOptions{})
2021-11-25 01:24:32 -05:00
if err != nil {
return err
}
}
return nil
}, priorityClassesCreateTimeout, pollInterval).Should(gomega.Succeed())
})
ginkgo.AfterEach(func() {
ginkgo.By("Emitting Shutdown false signal; cancelling the shutdown")
err := emitSignalPrepareForShutdown(false)
framework.ExpectNoError(err)
})
ginkgo.It("should be able to gracefully shutdown pods with various grace periods", func(ctx context.Context) {
nodeName := getNodeName(ctx, f)
nodeSelector := fields.Set{
"spec.nodeName": nodeName,
}.AsSelector().String()
var (
period5Name = "period-5-" + string(uuid.NewUUID())
periodC5Name = "period-c-5-" + string(uuid.NewUUID())
periodB5Name = "period-b-5-" + string(uuid.NewUUID())
periodA5Name = "period-a-5-" + string(uuid.NewUUID())
periodCritical5Name = "period-critical-5-" + string(uuid.NewUUID())
)
// Define test pods
pods := []*v1.Pod{
getGracePeriodOverrideTestPod(period5Name, nodeName, 5, ""),
getGracePeriodOverrideTestPod(periodC5Name, nodeName, 5, customClassC.Name),
getGracePeriodOverrideTestPod(periodB5Name, nodeName, 5, customClassB.Name),
getGracePeriodOverrideTestPod(periodA5Name, nodeName, 5, customClassA.Name),
getGracePeriodOverrideTestPod(periodCritical5Name, nodeName, 5, scheduling.SystemNodeCritical),
}
// Expected down steps
downSteps := [][]string{
{
period5Name,
},
{
period5Name,
periodC5Name,
},
{
period5Name,
periodC5Name,
periodB5Name,
},
{
period5Name,
periodC5Name,
periodB5Name,
periodA5Name,
},
{
period5Name,
periodC5Name,
periodB5Name,
periodA5Name,
periodCritical5Name,
},
}
ginkgo.By("Creating batch pods")
e2epod.NewPodClient(f).CreateBatch(ctx, pods)
list, err := e2epod.NewPodClient(f).List(ctx, metav1.ListOptions{
FieldSelector: nodeSelector,
})
framework.ExpectNoError(err)
gomega.Expect(list.Items).To(gomega.HaveLen(len(pods)), "the number of pods is not as expected")
ginkgo.By("Verifying batch pods are running")
for _, pod := range list.Items {
if podReady, err := testutils.PodRunningReady(&pod); err != nil || !podReady {
framework.Failf("Failed to start batch pod: (%v/%v)", pod.Namespace, pod.Name)
}
}
ginkgo.By("Emitting shutdown signal")
err = emitSignalPrepareForShutdown(true)
framework.ExpectNoError(err)
ginkgo.By("Verifying that pods are shutdown")
for _, step := range downSteps {
gomega.Eventually(ctx, func(ctx context.Context) error {
list, err = e2epod.NewPodClient(f).List(ctx, metav1.ListOptions{
FieldSelector: nodeSelector,
})
if err != nil {
return err
}
gomega.Expect(list.Items).To(gomega.HaveLen(len(pods)), "the number of pods is not as expected")
for _, pod := range list.Items {
shouldShutdown := false
for _, podName := range step {
if podName == pod.Name {
shouldShutdown = true
break
}
}
if !shouldShutdown {
if pod.Status.Phase != v1.PodRunning {
framework.Logf("Expecting pod to be running, but it's not currently. Pod: (%v/%v), Pod Status Phase: %q, Pod Status Reason: %q", pod.Namespace, pod.Name, pod.Status.Phase, pod.Status.Reason)
return fmt.Errorf("pod (%v/%v) should not be shutdown, phase: %s, reason: %s", pod.Namespace, pod.Name, pod.Status.Phase, pod.Status.Reason)
}
} else {
if pod.Status.Reason != podShutdownReason {
framework.Logf("Expecting pod to be shutdown, but it's not currently. Pod: (%v/%v), Pod Status Phase: %q, Pod Status Reason: %q", pod.Namespace, pod.Name, pod.Status.Phase, pod.Status.Reason)
for _, item := range list.Items {
framework.Logf("DEBUG %s, %s, %s", item.Name, item.Status.Phase, pod.Status.Reason)
}
return fmt.Errorf("pod (%v/%v) should be shutdown, reason: %s", pod.Namespace, pod.Name, pod.Status.Reason)
}
}
}
return nil
}, podStatusUpdateTimeout, pollInterval).Should(gomega.Succeed())
}
2022-03-11 04:30:10 -05:00
ginkgo.By("should have state file")
stateFile := "/var/lib/kubelet/graceful_node_shutdown_state"
_, err = os.Stat(stateFile)
framework.ExpectNoError(err)
})
})
2021-02-01 02:32:41 -05:00
})
func getPriorityClass(name string, value int32) *schedulingv1.PriorityClass {
priority := &schedulingv1.PriorityClass{
TypeMeta: metav1.TypeMeta{
Kind: "PriorityClass",
APIVersion: "scheduling.k8s.io/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Value: value,
}
return priority
}
// getGracePeriodOverrideTestPod returns a new Pod object containing a container
// runs a shell script, hangs the process until a SIGTERM signal is received.
// The script waits for $PID to ensure that the process does not exist.
// If priorityClassName is scheduling.SystemNodeCritical, the Pod is marked as critical and a comment is added.
func getGracePeriodOverrideTestPod(name string, node string, gracePeriod int64, priorityClassName string) *v1.Pod {
2021-02-01 02:32:41 -05:00
pod := &v1.Pod{
TypeMeta: metav1.TypeMeta{
Kind: "Pod",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: name,
Image: busyboxImage,
Command: []string{"sh", "-c"},
Args: []string{`
sleep 9999999 &
PID=$!
_term() {
echo "Caught SIGTERM signal!"
wait $PID
}
trap _term SIGTERM
wait $PID
`},
2021-02-01 02:32:41 -05:00
},
},
TerminationGracePeriodSeconds: &gracePeriod,
NodeName: node,
},
}
if priorityClassName == scheduling.SystemNodeCritical {
2021-02-01 02:32:41 -05:00
pod.ObjectMeta.Annotations = map[string]string{
kubelettypes.ConfigSourceAnnotationKey: kubelettypes.FileSource,
}
pod.Spec.PriorityClassName = priorityClassName
if !kubelettypes.IsCriticalPod(pod) {
framework.Failf("pod %q should be a critical pod", pod.Name)
}
2021-02-01 02:32:41 -05:00
} else {
pod.Spec.PriorityClassName = priorityClassName
if kubelettypes.IsCriticalPod(pod) {
framework.Failf("pod %q should not be a critical pod", pod.Name)
}
2021-02-01 02:32:41 -05:00
}
return pod
}
// Emits a fake PrepareForShutdown dbus message on system dbus. Will cause kubelet to react to an active shutdown event.
func emitSignalPrepareForShutdown(b bool) error {
conn, err := dbus.ConnectSystemBus()
if err != nil {
return err
}
defer conn.Close()
return conn.Emit("/org/freedesktop/login1", "org.freedesktop.login1.Manager.PrepareForShutdown", b)
}
const (
// https://github.com/kubernetes/kubernetes/blob/1dd781ddcad454cc381806fbc6bd5eba8fa368d7/pkg/kubelet/nodeshutdown/nodeshutdown_manager_linux.go#L43-L44
podShutdownReason = "Terminated"
podShutdownMessage = "Pod was terminated in response to imminent node shutdown."
)
func isPodShutdown(pod *v1.Pod) bool {
if pod == nil {
return false
}
hasContainersNotReadyCondition := false
for _, cond := range pod.Status.Conditions {
if cond.Type == v1.ContainersReady && cond.Status == v1.ConditionFalse {
hasContainersNotReadyCondition = true
}
}
return pod.Status.Message == podShutdownMessage && pod.Status.Reason == podShutdownReason && hasContainersNotReadyCondition && pod.Status.Phase == v1.PodFailed
}
// Pods should never report failed phase and have ready condition = true (https://github.com/kubernetes/kubernetes/issues/108594)
func isPodStatusAffectedByIssue108594(pod *v1.Pod) bool {
return pod.Status.Phase == v1.PodFailed && podutils.IsPodReady(pod)
}
func isPodReadyToStartConditionSetToFalse(pod *v1.Pod) bool {
if pod == nil {
return false
}
readyToStartConditionSetToFalse := false
for _, cond := range pod.Status.Conditions {
if cond.Status == v1.ConditionFalse {
readyToStartConditionSetToFalse = true
}
}
return readyToStartConditionSetToFalse
}