Skip to content

Commit 00cdbe8

Browse files
committed
OCPNODE-3932: Add automated tests for non-CNV swap configuration
Ran make verify-deps Skip for hypershift cluster
1 parent 1d1612d commit 00cdbe8

3 files changed

Lines changed: 270 additions & 1 deletion

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ require (
115115
k8s.io/kube-aggregator v0.34.1
116116
k8s.io/kube-openapi v0.0.0-20250814151709-d7b6acb124c3
117117
k8s.io/kubectl v0.34.1
118+
k8s.io/kubelet v0.31.1
118119
k8s.io/kubernetes v1.34.1
119120
k8s.io/pod-security-admission v0.34.1
120121
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
@@ -399,7 +400,6 @@ require (
399400
k8s.io/externaljwt v0.0.0 // indirect
400401
k8s.io/kms v0.34.1 // indirect
401402
k8s.io/kube-scheduler v0.0.0 // indirect
402-
k8s.io/kubelet v0.31.1 // indirect
403403
k8s.io/mount-utils v0.0.0 // indirect
404404
k8s.io/sample-apiserver v0.0.0 // indirect
405405
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0 // indirect

test/extended/node/node_swap.go

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package node
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
g "github.com/onsi/ginkgo/v2"
9+
o "github.com/onsi/gomega"
10+
ote "github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo"
11+
12+
corev1 "k8s.io/api/core/v1"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/apimachinery/pkg/runtime"
15+
"k8s.io/apimachinery/pkg/util/wait"
16+
"k8s.io/kubernetes/test/e2e/framework"
17+
18+
configv1 "github.com/openshift/api/config/v1"
19+
machineconfigv1 "github.com/openshift/api/machineconfiguration/v1"
20+
mcclient "github.com/openshift/client-go/machineconfiguration/clientset/versioned"
21+
exutil "github.com/openshift/origin/test/extended/util"
22+
)
23+
24+
const (
25+
workerGeneratedKubeletMC = "99-worker-generated-kubelet"
26+
)
27+
28+
var _ = g.Describe("[Jira:Node][sig-node] Node non-cnv swap configuration", func() {
29+
defer g.GinkgoRecover()
30+
31+
var oc = exutil.NewCLI("node-swap")
32+
33+
g.BeforeEach(func(ctx context.Context) {
34+
// Skip all tests on MicroShift clusters
35+
isMicroShift, err := exutil.IsMicroShiftCluster(oc.AdminKubeClient())
36+
o.Expect(err).NotTo(o.HaveOccurred())
37+
if isMicroShift {
38+
g.Skip("Skipping test on MicroShift cluster")
39+
}
40+
41+
// Skip on hypershift (External topology)
42+
controlPlaneTopology, err := exutil.GetControlPlaneTopology(oc)
43+
o.Expect(err).NotTo(o.HaveOccurred())
44+
if *controlPlaneTopology == configv1.ExternalTopologyMode {
45+
g.Skip("Skipping tests on Hypershift cluster")
46+
}
47+
})
48+
49+
// This test validates that:
50+
// - Worker nodes have failSwapOn=false to allow kubelet to start even if swap is present at OS level
51+
// - Control plane nodes have failSwapOn=true to prevent kubelet from starting if swap is enabled
52+
// - All nodes have swapBehavior=NoSwap to ensure kubelet does not utilize swap even if available at OS level
53+
// The swapBehavior=NoSwap configuration ensures that even if swap is manually enabled on a worker node,
54+
// the kubelet will not use it for memory management, maintaining consistent behavior across the cluster.
55+
g.It("should have correct default kubelet swap settings with worker nodes failSwapOn=false, control plane nodes failSwapOn=true, and both swapBehavior=NoSwap [OCP-86394]", ote.Informing(), func(ctx context.Context) {
56+
g.By("Getting worker nodes")
57+
workerNodes, err := getNodesByLabel(ctx, oc, "node-role.kubernetes.io/worker")
58+
o.Expect(err).NotTo(o.HaveOccurred())
59+
o.Expect(len(workerNodes)).Should(o.BeNumerically(">", 0), "Expected at least one worker node")
60+
61+
g.By("Validating kubelet configuration on each worker node")
62+
for _, node := range workerNodes {
63+
config, err := getKubeletConfigFromNode(ctx, oc, node.Name)
64+
o.Expect(err).NotTo(o.HaveOccurred(), "Failed to get kubelet config for worker node %s", node.Name)
65+
66+
g.By(fmt.Sprintf("Checking failSwapOn=false on worker node %s", node.Name))
67+
o.Expect(config.FailSwapOn).NotTo(o.BeNil(), "failSwapOn should be set on worker node %s", node.Name)
68+
o.Expect(*config.FailSwapOn).To(o.BeFalse(), "failSwapOn should be false on worker node %s", node.Name)
69+
framework.Logf("Worker node %s: failSwapOn=%v ✓", node.Name, *config.FailSwapOn)
70+
71+
g.By(fmt.Sprintf("Checking swapBehavior=NoSwap on worker node %s", node.Name))
72+
o.Expect(config.MemorySwap).NotTo(o.BeNil(), "memorySwap should be set on worker node %s", node.Name)
73+
o.Expect(config.MemorySwap.SwapBehavior).To(o.Equal("NoSwap"), "swapBehavior should be NoSwap on worker node %s", node.Name)
74+
framework.Logf("Worker node %s: swapBehavior=%s ✓", node.Name, config.MemorySwap.SwapBehavior)
75+
}
76+
77+
g.By("Getting control plane nodes")
78+
controlPlaneNodes, err := getControlPlaneNodes(ctx, oc)
79+
o.Expect(err).NotTo(o.HaveOccurred())
80+
o.Expect(len(controlPlaneNodes)).Should(o.BeNumerically(">", 0), "Expected at least one control plane node")
81+
82+
g.By("Validating kubelet configuration on each control plane node")
83+
for _, node := range controlPlaneNodes {
84+
config, err := getKubeletConfigFromNode(ctx, oc, node.Name)
85+
o.Expect(err).NotTo(o.HaveOccurred(), "Failed to get kubelet config for control plane node %s", node.Name)
86+
87+
g.By(fmt.Sprintf("Checking failSwapOn=true on control plane node %s", node.Name))
88+
o.Expect(config.FailSwapOn).NotTo(o.BeNil(), "failSwapOn should be set on control plane node %s", node.Name)
89+
o.Expect(*config.FailSwapOn).To(o.BeTrue(), "failSwapOn should be true on control plane node %s", node.Name)
90+
framework.Logf("Control plane node %s: failSwapOn=%v ✓", node.Name, *config.FailSwapOn)
91+
92+
g.By(fmt.Sprintf("Checking swapBehavior=NoSwap on control plane node %s", node.Name))
93+
o.Expect(config.MemorySwap).NotTo(o.BeNil(), "memorySwap should be set on control plane node %s", node.Name)
94+
o.Expect(config.MemorySwap.SwapBehavior).To(o.Equal("NoSwap"), "swapBehavior should be NoSwap on control plane node %s", node.Name)
95+
framework.Logf("Control plane node %s: swapBehavior=%s ✓", node.Name, config.MemorySwap.SwapBehavior)
96+
}
97+
framework.Logf("Test PASSED: All nodes have correct default swap settings")
98+
})
99+
100+
g.It("should reject user override of swap settings via KubeletConfig API [OCP-86395]", ote.Informing(), func(ctx context.Context) {
101+
g.By("Creating machine config client")
102+
mcClient, err := mcclient.NewForConfig(oc.KubeFramework().ClientConfig())
103+
o.Expect(err).NotTo(o.HaveOccurred(), "Failed to create machine config client")
104+
105+
g.By("Getting initial machine config resourceVersion")
106+
// Get the initial resourceVersion of the worker machine config before creating KubeletConfig
107+
workerMC, err := mcClient.MachineconfigurationV1().MachineConfigs().Get(ctx, workerGeneratedKubeletMC, metav1.GetOptions{})
108+
initialResourceVersion := ""
109+
if err == nil {
110+
initialResourceVersion = workerMC.ResourceVersion
111+
framework.Logf("Initial %s resourceVersion: %s", workerGeneratedKubeletMC, initialResourceVersion)
112+
}
113+
114+
g.By("Creating a KubeletConfig with swap settings")
115+
kubeletConfig := &machineconfigv1.KubeletConfig{
116+
ObjectMeta: metav1.ObjectMeta{
117+
Name: "test-swap-override",
118+
},
119+
Spec: machineconfigv1.KubeletConfigSpec{
120+
KubeletConfig: &runtime.RawExtension{
121+
Raw: []byte(`{
122+
"failSwapOn": true,
123+
"memorySwap": {
124+
"swapBehavior": "LimitedSwap"
125+
}
126+
}`),
127+
},
128+
},
129+
}
130+
131+
g.By("Attempting to apply the KubeletConfig")
132+
defer func() {
133+
_ = mcClient.MachineconfigurationV1().KubeletConfigs().Delete(ctx, "test-swap-override", metav1.DeleteOptions{})
134+
}()
135+
framework.Logf("Creating KubeletConfig with failSwapOn=true and swapBehavior=LimitedSwap")
136+
_, err = mcClient.MachineconfigurationV1().KubeletConfigs().Create(ctx, kubeletConfig, metav1.CreateOptions{})
137+
o.Expect(err).NotTo(o.HaveOccurred(), "Failed to create KubeletConfig")
138+
139+
g.By("Checking KubeletConfig status for expected error message")
140+
err = wait.Poll(2*time.Second, 30*time.Second, func() (bool, error) {
141+
kc, err := mcClient.MachineconfigurationV1().KubeletConfigs().Get(ctx, "test-swap-override", metav1.GetOptions{})
142+
if err != nil {
143+
return false, err
144+
}
145+
146+
if kc.Status.ObservedGeneration != kc.Generation {
147+
framework.Logf("Waiting for controller to process generation %d (current: %d)", kc.Generation, kc.Status.ObservedGeneration)
148+
return false, nil
149+
}
150+
151+
// Fail fast if KubeletConfig was unexpectedly accepted
152+
for _, condition := range kc.Status.Conditions {
153+
if condition.Type == machineconfigv1.KubeletConfigSuccess && condition.Status == corev1.ConditionTrue {
154+
return false, fmt.Errorf("KubeletConfig was unexpectedly accepted")
155+
}
156+
}
157+
158+
// Check for Failure condition with the expected error message
159+
for _, condition := range kc.Status.Conditions {
160+
if condition.Type == machineconfigv1.KubeletConfigFailure && condition.Status == corev1.ConditionTrue {
161+
framework.Logf("Found Failure condition: %s", condition.Message)
162+
if condition.Message == "Error: KubeletConfiguration: failSwapOn is not allowed to be set, but contains: true" {
163+
return true, nil
164+
}
165+
}
166+
}
167+
return false, nil
168+
})
169+
o.Expect(err).NotTo(o.HaveOccurred(), "Expected to find error message about failSwapOn not being allowed in KubeletConfig status")
170+
171+
g.By("Verifying machine config was not created or updated")
172+
// Wait a bit to ensure no update happens
173+
time.Sleep(5 * time.Second)
174+
175+
// Check if the machine config was created or updated (compare to initial resourceVersion captured earlier)
176+
workerMC, err = mcClient.MachineconfigurationV1().MachineConfigs().Get(ctx, workerGeneratedKubeletMC, metav1.GetOptions{})
177+
if err == nil {
178+
o.Expect(workerMC.ResourceVersion).To(o.Equal(initialResourceVersion), "Machine config %s should not be updated when failSwapOn is rejected", workerGeneratedKubeletMC)
179+
framework.Logf("Verified: %s was not updated (resourceVersion: %s)", workerGeneratedKubeletMC, workerMC.ResourceVersion)
180+
}
181+
182+
g.By("Verifying worker nodes still have correct swap settings")
183+
workerNodes, err := getNodesByLabel(ctx, oc, "node-role.kubernetes.io/worker")
184+
o.Expect(err).NotTo(o.HaveOccurred())
185+
o.Expect(len(workerNodes)).Should(o.BeNumerically(">", 0), "Expected at least one worker node")
186+
187+
for _, node := range workerNodes {
188+
config, err := getKubeletConfigFromNode(ctx, oc, node.Name)
189+
o.Expect(err).NotTo(o.HaveOccurred(), "Failed to get kubelet config for worker node %s", node.Name)
190+
191+
g.By(fmt.Sprintf("Verifying failSwapOn=false remains unchanged on worker node %s", node.Name))
192+
o.Expect(config.FailSwapOn).NotTo(o.BeNil(), "failSwapOn should be set on worker node %s", node.Name)
193+
o.Expect(*config.FailSwapOn).To(o.BeFalse(), "failSwapOn should still be false on worker node %s after rejection", node.Name)
194+
framework.Logf("Worker node %s: failSwapOn=%v (unchanged) ✓", node.Name, *config.FailSwapOn)
195+
196+
g.By(fmt.Sprintf("Verifying swapBehavior=NoSwap remains unchanged on worker node %s", node.Name))
197+
o.Expect(config.MemorySwap).NotTo(o.BeNil(), "memorySwap should be set on worker node %s", node.Name)
198+
o.Expect(config.MemorySwap.SwapBehavior).To(o.Equal("NoSwap"), "swapBehavior should still be NoSwap on worker node %s after rejection", node.Name)
199+
framework.Logf("Worker node %s: swapBehavior=%s (unchanged) ✓", node.Name, config.MemorySwap.SwapBehavior)
200+
}
201+
202+
framework.Logf("Test PASSED: KubeletConfig with failSwapOn was properly rejected")
203+
})
204+
})

test/extended/node/node_utils.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package node
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
v1 "k8s.io/api/core/v1"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
kubeletconfigv1beta1 "k8s.io/kubelet/config/v1beta1"
11+
12+
exutil "github.com/openshift/origin/test/extended/util"
13+
)
14+
15+
// getNodesByLabel returns nodes matching the specified label selector
16+
func getNodesByLabel(ctx context.Context, oc *exutil.CLI, labelSelector string) ([]v1.Node, error) {
17+
nodes, err := oc.AdminKubeClient().CoreV1().Nodes().List(ctx, metav1.ListOptions{
18+
LabelSelector: labelSelector,
19+
})
20+
if err != nil {
21+
return nil, err
22+
}
23+
return nodes.Items, nil
24+
}
25+
26+
// getControlPlaneNodes returns all control plane nodes in the cluster
27+
func getControlPlaneNodes(ctx context.Context, oc *exutil.CLI) ([]v1.Node, error) {
28+
// Try master label first (OpenShift uses this)
29+
nodes, err := getNodesByLabel(ctx, oc, "node-role.kubernetes.io/master")
30+
if err != nil {
31+
return nil, err
32+
}
33+
if len(nodes) > 0 {
34+
return nodes, nil
35+
}
36+
37+
// Fallback to control-plane label (upstream Kubernetes uses this)
38+
return getNodesByLabel(ctx, oc, "node-role.kubernetes.io/control-plane")
39+
}
40+
41+
// getKubeletConfigFromNode retrieves the kubelet configuration from a specific node
42+
func getKubeletConfigFromNode(ctx context.Context, oc *exutil.CLI, nodeName string) (*kubeletconfigv1beta1.KubeletConfiguration, error) {
43+
// Use the node proxy API to get configz
44+
configzPath := fmt.Sprintf("/api/v1/nodes/%s/proxy/configz", nodeName)
45+
46+
data, err := oc.AdminKubeClient().CoreV1().RESTClient().Get().AbsPath(configzPath).DoRaw(ctx)
47+
if err != nil {
48+
return nil, fmt.Errorf("failed to get configz from node %s: %w", nodeName, err)
49+
}
50+
51+
// Parse the JSON response
52+
var configzResponse struct {
53+
KubeletConfig *kubeletconfigv1beta1.KubeletConfiguration `json:"kubeletconfig"`
54+
}
55+
56+
if err := json.Unmarshal(data, &configzResponse); err != nil {
57+
return nil, fmt.Errorf("failed to unmarshal configz response: %w", err)
58+
}
59+
60+
if configzResponse.KubeletConfig == nil {
61+
return nil, fmt.Errorf("kubeletconfig is nil in response")
62+
}
63+
64+
return configzResponse.KubeletConfig, nil
65+
}

0 commit comments

Comments
 (0)