/* 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" "path/filepath" "strconv" "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/rand" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/pkg/kubelet/types" "k8s.io/kubernetes/test/e2e/framework" e2epod "k8s.io/kubernetes/test/e2e/framework/pod" testutils "k8s.io/kubernetes/test/utils" admissionapi "k8s.io/pod-security-admission/api" ) const ( cgroupBasePath = "/sys/fs/cgroup/" cgroupV1SwapLimitFile = "/memory/memory.memsw.limit_in_bytes" cgroupV2SwapLimitFile = "memory.swap.max" cgroupV1MemLimitFile = "/memory/memory.limit_in_bytes" ) var _ = SIGDescribe("Swap", framework.WithNodeConformance(), "[LinuxOnly]", func() { f := framework.NewDefaultFramework("swap-test") f.NamespacePodSecurityEnforceLevel = admissionapi.LevelBaseline ginkgo.DescribeTable("with configuration", func(qosClass v1.PodQOSClass, memoryRequestEqualLimit bool) { ginkgo.By(fmt.Sprintf("Creating a pod of QOS class %s. memoryRequestEqualLimit: %t", qosClass, memoryRequestEqualLimit)) pod := getSwapTestPod(f, qosClass, memoryRequestEqualLimit) pod = runPodAndWaitUntilScheduled(f, pod) isCgroupV2 := isPodCgroupV2(f, pod) isLimitedSwap := isLimitedSwap(f, pod) isNoSwap := isNoSwap(f, pod) if !isSwapFeatureGateEnabled() || !isCgroupV2 || isNoSwap || (isLimitedSwap && (qosClass != v1.PodQOSBurstable || memoryRequestEqualLimit)) { ginkgo.By(fmt.Sprintf("Expecting no swap. isNoSwap? %t, feature gate on? %t isCgroupV2? %t is QoS burstable? %t", isNoSwap, isSwapFeatureGateEnabled(), isCgroupV2, qosClass == v1.PodQOSBurstable)) expectNoSwap(f, pod, isCgroupV2) return } if !isLimitedSwap { ginkgo.By("expecting no swap") expectNoSwap(f, pod, isCgroupV2) return } ginkgo.By("expecting limited swap") expectedSwapLimit := calcSwapForBurstablePod(f, pod) expectLimitedSwap(f, pod, expectedSwapLimit) }, ginkgo.Entry("QOS Best-effort", v1.PodQOSBestEffort, false), ginkgo.Entry("QOS Burstable", v1.PodQOSBurstable, false), ginkgo.Entry("QOS Burstable with memory request equals to limit", v1.PodQOSBurstable, true), ginkgo.Entry("QOS Guaranteed", v1.PodQOSGuaranteed, false), ) }) // Note that memoryRequestEqualLimit is effective only when qosClass is PodQOSBestEffort. func getSwapTestPod(f *framework.Framework, qosClass v1.PodQOSClass, memoryRequestEqualLimit bool) *v1.Pod { podMemoryAmount := resource.MustParse("128Mi") var resources v1.ResourceRequirements switch qosClass { case v1.PodQOSBestEffort: // nothing to do in this case case v1.PodQOSBurstable: resources = v1.ResourceRequirements{ Requests: v1.ResourceList{ v1.ResourceMemory: podMemoryAmount, }, } if memoryRequestEqualLimit { resources.Limits = resources.Requests } case v1.PodQOSGuaranteed: resources = v1.ResourceRequirements{ Limits: v1.ResourceList{ v1.ResourceCPU: resource.MustParse("200m"), v1.ResourceMemory: podMemoryAmount, }, } resources.Requests = resources.Limits } pod := &v1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "test-pod-swap-" + rand.String(5), Namespace: f.Namespace.Name, }, Spec: v1.PodSpec{ RestartPolicy: v1.RestartPolicyAlways, Containers: []v1.Container{ { Name: "busybox-container", Image: busyboxImage, Command: []string{"sleep", "600"}, Resources: resources, }, }, }, } return pod } func runPodAndWaitUntilScheduled(f *framework.Framework, pod *v1.Pod) *v1.Pod { ginkgo.By("running swap test pod") podClient := e2epod.NewPodClient(f) pod = podClient.CreateSync(context.Background(), pod) pod, err := podClient.Get(context.Background(), pod.Name, metav1.GetOptions{}) framework.ExpectNoError(err) isReady, err := testutils.PodRunningReady(pod) framework.ExpectNoError(err) gomega.ExpectWithOffset(1, isReady).To(gomega.BeTrue(), "pod should be ready") return pod } func isSwapFeatureGateEnabled() bool { ginkgo.By("figuring if NodeSwap feature gate is turned on") return utilfeature.DefaultFeatureGate.Enabled(features.NodeSwap) } func readCgroupFile(f *framework.Framework, pod *v1.Pod, filename string) string { filePath := filepath.Join(cgroupBasePath, filename) ginkgo.By("reading cgroup file " + filePath) output := e2epod.ExecCommandInContainer(f, pod.Name, pod.Spec.Containers[0].Name, "/bin/sh", "-ec", "cat "+filePath) return output } func isPodCgroupV2(f *framework.Framework, pod *v1.Pod) bool { ginkgo.By("figuring is test pod runs cgroup v2") output := e2epod.ExecCommandInContainer(f, pod.Name, pod.Spec.Containers[0].Name, "/bin/sh", "-ec", `if test -f "/sys/fs/cgroup/cgroup.controllers"; then echo "true"; else echo "false"; fi`) return output == "true" } func expectNoSwap(f *framework.Framework, pod *v1.Pod, isCgroupV2 bool) { if isCgroupV2 { swapLimit := readCgroupFile(f, pod, cgroupV2SwapLimitFile) gomega.ExpectWithOffset(1, swapLimit).To(gomega.Equal("0"), "max swap allowed should be zero") } else { swapPlusMemLimit := readCgroupFile(f, pod, cgroupV1SwapLimitFile) memLimit := readCgroupFile(f, pod, cgroupV1MemLimitFile) gomega.ExpectWithOffset(1, swapPlusMemLimit).ToNot(gomega.BeEmpty()) gomega.ExpectWithOffset(1, swapPlusMemLimit).To(gomega.Equal(memLimit)) } } // supports v2 only as v1 shouldn't support LimitedSwap func expectLimitedSwap(f *framework.Framework, pod *v1.Pod, expectedSwapLimit int64) { swapLimitStr := readCgroupFile(f, pod, cgroupV2SwapLimitFile) swapLimit, err := strconv.Atoi(swapLimitStr) framework.ExpectNoError(err, "cannot convert swap limit to int") // cgroup values are always aligned w.r.t. the page size, which is usually 4Ki const cgroupAlignment int64 = 4 * 1024 // 4Ki const errMsg = "swap limitation is not as expected" gomega.ExpectWithOffset(1, int64(swapLimit)).To( gomega.Or( gomega.BeNumerically(">=", expectedSwapLimit-cgroupAlignment), gomega.BeNumerically("<=", expectedSwapLimit+cgroupAlignment), ), errMsg, ) } func getSwapCapacity(f *framework.Framework, pod *v1.Pod) int64 { output := e2epod.ExecCommandInContainer(f, pod.Name, pod.Spec.Containers[0].Name, "/bin/sh", "-ec", "free -b | grep Swap | xargs | cut -d\" \" -f2") swapCapacity, err := strconv.Atoi(output) framework.ExpectNoError(err, "cannot convert swap size to int") ginkgo.By(fmt.Sprintf("providing swap capacity: %d", swapCapacity)) return int64(swapCapacity) } func getMemoryCapacity(f *framework.Framework, pod *v1.Pod) int64 { nodes, err := f.ClientSet.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{}) framework.ExpectNoError(err, "failed listing nodes") for _, node := range nodes.Items { if node.Name != pod.Spec.NodeName { continue } memCapacity := node.Status.Capacity[v1.ResourceMemory] return memCapacity.Value() } framework.ExpectNoError(fmt.Errorf("node %s wasn't found", pod.Spec.NodeName)) return 0 } func calcSwapForBurstablePod(f *framework.Framework, pod *v1.Pod) int64 { nodeMemoryCapacity := getMemoryCapacity(f, pod) nodeSwapCapacity := getSwapCapacity(f, pod) containerMemoryRequest := pod.Spec.Containers[0].Resources.Requests.Memory().Value() containerMemoryProportion := float64(containerMemoryRequest) / float64(nodeMemoryCapacity) swapAllocation := containerMemoryProportion * float64(nodeSwapCapacity) ginkgo.By(fmt.Sprintf("Calculating swap for burstable pods: nodeMemoryCapacity: %d, nodeSwapCapacity: %d, containerMemoryRequest: %d, swapAllocation: %d", nodeMemoryCapacity, nodeSwapCapacity, containerMemoryRequest, int64(swapAllocation))) return int64(swapAllocation) } func isLimitedSwap(f *framework.Framework, pod *v1.Pod) bool { kubeletCfg, err := getCurrentKubeletConfig(context.Background()) framework.ExpectNoError(err, "cannot get kubelet config") return kubeletCfg.MemorySwap.SwapBehavior == types.LimitedSwap } func isNoSwap(f *framework.Framework, pod *v1.Pod) bool { kubeletCfg, err := getCurrentKubeletConfig(context.Background()) framework.ExpectNoError(err, "cannot get kubelet config") return kubeletCfg.MemorySwap.SwapBehavior == types.NoSwap || kubeletCfg.MemorySwap.SwapBehavior == "" }